拆分兑换凭证渲染与截图逻辑
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import {
|
||||||
|
BAIDU_BEIJING_TIME_URL,
|
||||||
|
BEIJING_TIME_PROOF_CLIP,
|
||||||
|
BEIJING_TIME_PROOF_VIEWPORT,
|
||||||
|
} from './session-proof-constants.js'
|
||||||
|
|
||||||
|
export async function captureBeijingTimeProof(browserContext, { screenshotPath }) {
|
||||||
|
const proofPage = await browserContext.newPage()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await proofPage.setViewportSize(BEIJING_TIME_PROOF_VIEWPORT)
|
||||||
|
await proofPage.goto(BAIDU_BEIJING_TIME_URL, { waitUntil: 'domcontentloaded' })
|
||||||
|
await proofPage.waitForTimeout(3_000)
|
||||||
|
await proofPage.evaluate(() => {
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
})
|
||||||
|
await proofPage.screenshot({
|
||||||
|
path: screenshotPath,
|
||||||
|
clip: BEIJING_TIME_PROOF_CLIP,
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
screenshotPath,
|
||||||
|
pageUrl: BAIDU_BEIJING_TIME_URL,
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await proofPage.close().catch(() => null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
export function buildProofHtml({ redeemImageBase64, timeImageBase64, redeemMessage, timePageUrl }) {
|
||||||
|
const capturedAt = new Date().toLocaleString('zh-CN', {
|
||||||
|
hour12: false,
|
||||||
|
timeZone: 'Asia/Shanghai',
|
||||||
|
})
|
||||||
|
|
||||||
|
return `<!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 {
|
||||||
|
color-scheme: light;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(33, 150, 243, 0.16), transparent 28%),
|
||||||
|
linear-gradient(180deg, #edf4ff 0%, #f6f8fc 52%, #eef1f7 100%);
|
||||||
|
color: #142033;
|
||||||
|
}
|
||||||
|
.sheet {
|
||||||
|
width: 1480px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 30px 24px 28px;
|
||||||
|
}
|
||||||
|
.hero {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-end;
|
||||||
|
padding: 0 6px 18px;
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
margin: 0 0 12px;
|
||||||
|
color: #1c78d0;
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 44px;
|
||||||
|
line-height: 1.08;
|
||||||
|
}
|
||||||
|
.summary {
|
||||||
|
margin: 14px 0 0;
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #52627a;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
min-width: 320px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
border-radius: 24px;
|
||||||
|
background: rgba(255, 255, 255, 0.76);
|
||||||
|
border: 1px solid rgba(20, 32, 51, 0.08);
|
||||||
|
box-shadow: 0 18px 48px rgba(29, 55, 90, 0.08);
|
||||||
|
}
|
||||||
|
.meta strong,
|
||||||
|
.meta span {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
.meta strong {
|
||||||
|
font-size: 13px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #60748d;
|
||||||
|
}
|
||||||
|
.meta span {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 20px;
|
||||||
|
color: #142033;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
margin-top: 16px;
|
||||||
|
padding: 20px;
|
||||||
|
border-radius: 28px;
|
||||||
|
background: rgba(255, 255, 255, 0.82);
|
||||||
|
border: 1px solid rgba(20, 32, 51, 0.08);
|
||||||
|
box-shadow: 0 24px 80px rgba(24, 41, 72, 0.1);
|
||||||
|
}
|
||||||
|
.card h2 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 30px;
|
||||||
|
}
|
||||||
|
.card p {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #5a6b83;
|
||||||
|
line-height: 1.65;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.preview {
|
||||||
|
margin-top: 14px;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 20px;
|
||||||
|
border: 1px solid rgba(20, 32, 51, 0.08);
|
||||||
|
background: #dfe7f3;
|
||||||
|
}
|
||||||
|
.preview img {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.preview--time {
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
.preview--time img {
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top center;
|
||||||
|
}
|
||||||
|
.url {
|
||||||
|
margin-top: 10px;
|
||||||
|
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #58708f;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main class="sheet">
|
||||||
|
<section class="hero">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Tencent Redeem Proof</p>
|
||||||
|
<h1>兑换截图与北京时间截图</h1>
|
||||||
|
<p class="summary">用于留存兑换结果与北京时间检索页的组合凭证。</p>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<strong>Captured At</strong>
|
||||||
|
<span>${escapeHtml(capturedAt)}</span>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="card">
|
||||||
|
<h2>兑换结果截图</h2>
|
||||||
|
<p>${escapeHtml(redeemMessage || '兑换完成')}</p>
|
||||||
|
<div class="preview">
|
||||||
|
<img src="data:image/png;base64,${redeemImageBase64}" alt="兑换结果截图" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="card">
|
||||||
|
<h2>百度检索“北京时间”截图</h2>
|
||||||
|
<p>单独标签页打开百度搜索结果并截图。</p>
|
||||||
|
<div class="url">${escapeHtml(timePageUrl)}</div>
|
||||||
|
<div class="preview preview--time">
|
||||||
|
<img src="data:image/png;base64,${timeImageBase64}" alt="北京时间截图" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeHtml(value) {
|
||||||
|
return String(value || '')
|
||||||
|
.replaceAll('&', '&')
|
||||||
|
.replaceAll('<', '<')
|
||||||
|
.replaceAll('>', '>')
|
||||||
|
.replaceAll('"', '"')
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// @ts-nocheck
|
||||||
|
|
||||||
|
import fs from 'node:fs/promises'
|
||||||
|
|
||||||
|
import { buildProofHtml } from './session-proof-html.js'
|
||||||
|
|
||||||
|
export async function showResultDialog(page, message) {
|
||||||
|
await page.evaluate((text) => {
|
||||||
|
document.querySelectorAll('iframe').forEach((element) => element.remove())
|
||||||
|
document.querySelectorAll('.pop').forEach((element) => {
|
||||||
|
element.style.display = 'none'
|
||||||
|
})
|
||||||
|
|
||||||
|
let card = document.getElementById('codex-redeem-result')
|
||||||
|
|
||||||
|
if (!card) {
|
||||||
|
card = document.createElement('div')
|
||||||
|
card.id = 'codex-redeem-result'
|
||||||
|
card.innerHTML = `
|
||||||
|
<div class="codex-redeem-result__eyebrow">Tencent Browser Session</div>
|
||||||
|
<div class="codex-redeem-result__title">兑换结果</div>
|
||||||
|
<div class="codex-redeem-result__message"></div>
|
||||||
|
`
|
||||||
|
document.body.appendChild(card)
|
||||||
|
}
|
||||||
|
|
||||||
|
const messageNode = card.querySelector('.codex-redeem-result__message')
|
||||||
|
|
||||||
|
if (messageNode) {
|
||||||
|
messageNode.textContent = text
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(card.style, {
|
||||||
|
position: 'fixed',
|
||||||
|
left: '50%',
|
||||||
|
top: '50%',
|
||||||
|
transform: 'translate(-50%, -50%)',
|
||||||
|
zIndex: '99999',
|
||||||
|
width: 'min(560px, calc(100vw - 80px))',
|
||||||
|
padding: '32px 36px',
|
||||||
|
borderRadius: '24px',
|
||||||
|
background: 'rgba(10, 18, 28, 0.92)',
|
||||||
|
boxShadow: '0 24px 80px rgba(0, 0, 0, 0.35)',
|
||||||
|
border: '1px solid rgba(109, 241, 202, 0.25)',
|
||||||
|
color: '#f4fbff',
|
||||||
|
fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
|
||||||
|
})
|
||||||
|
|
||||||
|
const styleId = 'codex-redeem-result-style'
|
||||||
|
|
||||||
|
if (!document.getElementById(styleId)) {
|
||||||
|
const style = document.createElement('style')
|
||||||
|
style.id = styleId
|
||||||
|
style.textContent = `
|
||||||
|
#codex-redeem-result .codex-redeem-result__eyebrow {
|
||||||
|
font-size: 12px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: #6df1ca;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
#codex-redeem-result .codex-redeem-result__title {
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
#codex-redeem-result .codex-redeem-result__message {
|
||||||
|
font-size: 24px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
document.head.appendChild(style)
|
||||||
|
}
|
||||||
|
|
||||||
|
window.scrollTo(0, 0)
|
||||||
|
}, message)
|
||||||
|
|
||||||
|
await page.waitForTimeout(600)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function composeProofScreenshot(
|
||||||
|
browserContext,
|
||||||
|
{ outputPath, redeemImagePath, timeImagePath, redeemMessage, timePageUrl },
|
||||||
|
) {
|
||||||
|
const [redeemImageBase64, timeImageBase64] = await Promise.all([
|
||||||
|
fs.readFile(redeemImagePath, 'base64'),
|
||||||
|
fs.readFile(timeImagePath, 'base64'),
|
||||||
|
])
|
||||||
|
|
||||||
|
const composePage = await browserContext.newPage()
|
||||||
|
|
||||||
|
try {
|
||||||
|
await composePage.setViewportSize({ width: 1520, height: 1900 })
|
||||||
|
await composePage.setContent(
|
||||||
|
buildProofHtml({
|
||||||
|
redeemImageBase64,
|
||||||
|
timeImageBase64,
|
||||||
|
redeemMessage,
|
||||||
|
timePageUrl,
|
||||||
|
}),
|
||||||
|
{ waitUntil: 'domcontentloaded' },
|
||||||
|
)
|
||||||
|
await composePage.screenshot({ path: outputPath, fullPage: true })
|
||||||
|
} finally {
|
||||||
|
await composePage.close().catch(() => null)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,10 +4,10 @@ import fs from 'node:fs/promises'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
BAIDU_BEIJING_TIME_URL,
|
BAIDU_BEIJING_TIME_URL,
|
||||||
BEIJING_TIME_PROOF_CLIP,
|
|
||||||
BEIJING_TIME_PROOF_VIEWPORT,
|
|
||||||
DEFAULT_REDEEM_PROOF_MODE,
|
DEFAULT_REDEEM_PROOF_MODE,
|
||||||
} from './session-proof-constants.js'
|
} from './session-proof-constants.js'
|
||||||
|
import { captureBeijingTimeProof } from './session-proof-beijing-time.js'
|
||||||
|
import { showResultDialog, composeProofScreenshot } from './session-proof-renderer.js'
|
||||||
import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof-mode.js'
|
import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof-mode.js'
|
||||||
import { buildArtifactPaths } from './session-proof-paths.js'
|
import { buildArtifactPaths } from './session-proof-paths.js'
|
||||||
import { writeRedeemResultFile } from './session-proof-result-writer.js'
|
import { writeRedeemResultFile } from './session-proof-result-writer.js'
|
||||||
@@ -143,299 +143,3 @@ export async function writeRedeemArtifactFiles({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export { resolveRedeemProofMode, shouldCaptureBeijingTimeProof }
|
export { resolveRedeemProofMode, shouldCaptureBeijingTimeProof }
|
||||||
|
|
||||||
async function showResultDialog(page, message) {
|
|
||||||
await page.evaluate((text) => {
|
|
||||||
document.querySelectorAll('iframe').forEach((element) => element.remove())
|
|
||||||
document.querySelectorAll('.pop').forEach((element) => {
|
|
||||||
element.style.display = 'none'
|
|
||||||
})
|
|
||||||
|
|
||||||
let card = document.getElementById('codex-redeem-result')
|
|
||||||
|
|
||||||
if (!card) {
|
|
||||||
card = document.createElement('div')
|
|
||||||
card.id = 'codex-redeem-result'
|
|
||||||
card.innerHTML = `
|
|
||||||
<div class="codex-redeem-result__eyebrow">Tencent Browser Session</div>
|
|
||||||
<div class="codex-redeem-result__title">兑换结果</div>
|
|
||||||
<div class="codex-redeem-result__message"></div>
|
|
||||||
`
|
|
||||||
document.body.appendChild(card)
|
|
||||||
}
|
|
||||||
|
|
||||||
const messageNode = card.querySelector('.codex-redeem-result__message')
|
|
||||||
|
|
||||||
if (messageNode) {
|
|
||||||
messageNode.textContent = text
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(card.style, {
|
|
||||||
position: 'fixed',
|
|
||||||
left: '50%',
|
|
||||||
top: '50%',
|
|
||||||
transform: 'translate(-50%, -50%)',
|
|
||||||
zIndex: '99999',
|
|
||||||
width: 'min(560px, calc(100vw - 80px))',
|
|
||||||
padding: '32px 36px',
|
|
||||||
borderRadius: '24px',
|
|
||||||
background: 'rgba(10, 18, 28, 0.92)',
|
|
||||||
boxShadow: '0 24px 80px rgba(0, 0, 0, 0.35)',
|
|
||||||
border: '1px solid rgba(109, 241, 202, 0.25)',
|
|
||||||
color: '#f4fbff',
|
|
||||||
fontFamily: '"PingFang SC", "Microsoft YaHei", sans-serif',
|
|
||||||
})
|
|
||||||
|
|
||||||
const styleId = 'codex-redeem-result-style'
|
|
||||||
|
|
||||||
if (!document.getElementById(styleId)) {
|
|
||||||
const style = document.createElement('style')
|
|
||||||
style.id = styleId
|
|
||||||
style.textContent = `
|
|
||||||
#codex-redeem-result .codex-redeem-result__eyebrow {
|
|
||||||
font-size: 12px;
|
|
||||||
letter-spacing: 0.18em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #6df1ca;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
#codex-redeem-result .codex-redeem-result__title {
|
|
||||||
font-size: 34px;
|
|
||||||
font-weight: 700;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
#codex-redeem-result .codex-redeem-result__message {
|
|
||||||
font-size: 24px;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
`
|
|
||||||
document.head.appendChild(style)
|
|
||||||
}
|
|
||||||
|
|
||||||
window.scrollTo(0, 0)
|
|
||||||
}, message)
|
|
||||||
|
|
||||||
await page.waitForTimeout(600)
|
|
||||||
}
|
|
||||||
|
|
||||||
async function captureBeijingTimeProof(browserContext, { screenshotPath }) {
|
|
||||||
const proofPage = await browserContext.newPage()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await proofPage.setViewportSize(BEIJING_TIME_PROOF_VIEWPORT)
|
|
||||||
await proofPage.goto(BAIDU_BEIJING_TIME_URL, { waitUntil: 'domcontentloaded' })
|
|
||||||
await proofPage.waitForTimeout(3_000)
|
|
||||||
await proofPage.evaluate(() => {
|
|
||||||
window.scrollTo(0, 0)
|
|
||||||
})
|
|
||||||
await proofPage.screenshot({
|
|
||||||
path: screenshotPath,
|
|
||||||
clip: BEIJING_TIME_PROOF_CLIP,
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
screenshotPath,
|
|
||||||
pageUrl: BAIDU_BEIJING_TIME_URL,
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
await proofPage.close().catch(() => null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function composeProofScreenshot(
|
|
||||||
browserContext,
|
|
||||||
{ outputPath, redeemImagePath, timeImagePath, redeemMessage, timePageUrl },
|
|
||||||
) {
|
|
||||||
const [redeemImageBase64, timeImageBase64] = await Promise.all([
|
|
||||||
fs.readFile(redeemImagePath, 'base64'),
|
|
||||||
fs.readFile(timeImagePath, 'base64'),
|
|
||||||
])
|
|
||||||
|
|
||||||
const composePage = await browserContext.newPage()
|
|
||||||
|
|
||||||
try {
|
|
||||||
await composePage.setViewportSize({ width: 1520, height: 1900 })
|
|
||||||
await composePage.setContent(
|
|
||||||
buildProofHtml({
|
|
||||||
redeemImageBase64,
|
|
||||||
timeImageBase64,
|
|
||||||
redeemMessage,
|
|
||||||
timePageUrl,
|
|
||||||
}),
|
|
||||||
{ waitUntil: 'domcontentloaded' },
|
|
||||||
)
|
|
||||||
await composePage.screenshot({ path: outputPath, fullPage: true })
|
|
||||||
} finally {
|
|
||||||
await composePage.close().catch(() => null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildProofHtml({ redeemImageBase64, timeImageBase64, redeemMessage, timePageUrl }) {
|
|
||||||
const capturedAt = new Date().toLocaleString('zh-CN', {
|
|
||||||
hour12: false,
|
|
||||||
timeZone: 'Asia/Shanghai',
|
|
||||||
})
|
|
||||||
|
|
||||||
return `<!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 {
|
|
||||||
color-scheme: light;
|
|
||||||
}
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
||||||
background:
|
|
||||||
radial-gradient(circle at top left, rgba(33, 150, 243, 0.16), transparent 28%),
|
|
||||||
linear-gradient(180deg, #edf4ff 0%, #f6f8fc 52%, #eef1f7 100%);
|
|
||||||
color: #142033;
|
|
||||||
}
|
|
||||||
.sheet {
|
|
||||||
width: 1480px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 30px 24px 28px;
|
|
||||||
}
|
|
||||||
.hero {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 20px;
|
|
||||||
align-items: flex-end;
|
|
||||||
padding: 0 6px 18px;
|
|
||||||
}
|
|
||||||
.eyebrow {
|
|
||||||
margin: 0 0 12px;
|
|
||||||
color: #1c78d0;
|
|
||||||
font-size: 13px;
|
|
||||||
letter-spacing: 0.2em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 44px;
|
|
||||||
line-height: 1.08;
|
|
||||||
}
|
|
||||||
.summary {
|
|
||||||
margin: 14px 0 0;
|
|
||||||
font-size: 18px;
|
|
||||||
line-height: 1.7;
|
|
||||||
color: #52627a;
|
|
||||||
}
|
|
||||||
.meta {
|
|
||||||
min-width: 320px;
|
|
||||||
padding: 18px 20px;
|
|
||||||
border-radius: 24px;
|
|
||||||
background: rgba(255, 255, 255, 0.76);
|
|
||||||
border: 1px solid rgba(20, 32, 51, 0.08);
|
|
||||||
box-shadow: 0 18px 48px rgba(29, 55, 90, 0.08);
|
|
||||||
}
|
|
||||||
.meta strong,
|
|
||||||
.meta span {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.meta strong {
|
|
||||||
font-size: 13px;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
color: #60748d;
|
|
||||||
}
|
|
||||||
.meta span {
|
|
||||||
margin-top: 8px;
|
|
||||||
font-size: 20px;
|
|
||||||
color: #142033;
|
|
||||||
line-height: 1.5;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
.card {
|
|
||||||
margin-top: 16px;
|
|
||||||
padding: 20px;
|
|
||||||
border-radius: 28px;
|
|
||||||
background: rgba(255, 255, 255, 0.82);
|
|
||||||
border: 1px solid rgba(20, 32, 51, 0.08);
|
|
||||||
box-shadow: 0 24px 80px rgba(24, 41, 72, 0.1);
|
|
||||||
}
|
|
||||||
.card h2 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 30px;
|
|
||||||
}
|
|
||||||
.card p {
|
|
||||||
margin: 8px 0 0;
|
|
||||||
color: #5a6b83;
|
|
||||||
line-height: 1.65;
|
|
||||||
font-size: 16px;
|
|
||||||
}
|
|
||||||
.preview {
|
|
||||||
margin-top: 14px;
|
|
||||||
overflow: hidden;
|
|
||||||
border-radius: 20px;
|
|
||||||
border: 1px solid rgba(20, 32, 51, 0.08);
|
|
||||||
background: #dfe7f3;
|
|
||||||
}
|
|
||||||
.preview img {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
}
|
|
||||||
.preview--time {
|
|
||||||
max-height: 500px;
|
|
||||||
}
|
|
||||||
.preview--time img {
|
|
||||||
object-fit: cover;
|
|
||||||
object-position: top center;
|
|
||||||
}
|
|
||||||
.url {
|
|
||||||
margin-top: 10px;
|
|
||||||
font-family: ui-monospace, "SFMono-Regular", Menlo, monospace;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #58708f;
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<main class="sheet">
|
|
||||||
<section class="hero">
|
|
||||||
<div>
|
|
||||||
<p class="eyebrow">Tencent Redeem Proof</p>
|
|
||||||
<h1>兑换截图与北京时间截图</h1>
|
|
||||||
<p class="summary">用于留存兑换结果与北京时间检索页的组合凭证。</p>
|
|
||||||
</div>
|
|
||||||
<div class="meta">
|
|
||||||
<strong>Captured At</strong>
|
|
||||||
<span>${escapeHtml(capturedAt)}</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="card">
|
|
||||||
<h2>兑换结果截图</h2>
|
|
||||||
<p>${escapeHtml(redeemMessage || '兑换完成')}</p>
|
|
||||||
<div class="preview">
|
|
||||||
<img src="data:image/png;base64,${redeemImageBase64}" alt="兑换结果截图" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
<section class="card">
|
|
||||||
<h2>百度检索“北京时间”截图</h2>
|
|
||||||
<p>单独标签页打开百度搜索结果并截图。</p>
|
|
||||||
<div class="url">${escapeHtml(timePageUrl)}</div>
|
|
||||||
<div class="preview preview--time">
|
|
||||||
<img src="data:image/png;base64,${timeImageBase64}" alt="北京时间截图" />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>`
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value) {
|
|
||||||
return String(value || '')
|
|
||||||
.replaceAll('&', '&')
|
|
||||||
.replaceAll('<', '<')
|
|
||||||
.replaceAll('>', '>')
|
|
||||||
.replaceAll('"', '"')
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof.js'
|
import { resolveRedeemProofMode, shouldCaptureBeijingTimeProof } from './session-proof.js'
|
||||||
|
import { buildProofHtml } from './session-proof-html.js'
|
||||||
import { buildArtifactPaths } from './session-proof-paths.js'
|
import { buildArtifactPaths } from './session-proof-paths.js'
|
||||||
|
|
||||||
test('resolveRedeemProofMode normalizes configured modes and falls back to full', () => {
|
test('resolveRedeemProofMode normalizes configured modes and falls back to full', () => {
|
||||||
@@ -62,3 +63,19 @@ test('buildArtifactPaths keeps redeem proof artifact filenames stable', () => {
|
|||||||
assert.equal(artifactPaths.htmlPath, path.join('/tmp/redeem-proof', 'page.html'))
|
assert.equal(artifactPaths.htmlPath, path.join('/tmp/redeem-proof', 'page.html'))
|
||||||
assert.equal(artifactPaths.resultPath, path.join('/tmp/redeem-proof', 'result.json'))
|
assert.equal(artifactPaths.resultPath, path.join('/tmp/redeem-proof', 'result.json'))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('buildProofHtml escapes injected text while keeping proof sections stable', () => {
|
||||||
|
const html = buildProofHtml({
|
||||||
|
redeemImageBase64: 'redeem-base64',
|
||||||
|
timeImageBase64: 'time-base64',
|
||||||
|
redeemMessage: '<b>兑换成功 & 已到账</b>',
|
||||||
|
timePageUrl: 'https://example.com/?q=<北京时间>',
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.match(html, /Tencent Redeem Proof/)
|
||||||
|
assert.match(html, /兑换截图与北京时间截图/)
|
||||||
|
assert.match(html, /<b>兑换成功 & 已到账<\/b>/)
|
||||||
|
assert.match(html, /https:\/\/example\.com\/\?q=<北京时间>/)
|
||||||
|
assert.match(html, /data:image\/png;base64,redeem-base64/)
|
||||||
|
assert.match(html, /data:image\/png;base64,time-base64/)
|
||||||
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user