<!DOCTYPE html>
<html lang=”zh-CN”>
<head>
<meta charset=”UTF-8″>
<meta name=”viewport” content=”width=device-width, initial-scale=1.0″>
<title>牛津英语连连看 – 在游戏中掌握英语</title>
<link href=”https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800&family=Noto+Sans+SC:wght@400;500;700&display=swap” rel=”stylesheet”>
<style>
:root {
–primary: #4CAF50;
–primary-dark: #388E3C;
–primary-light: #81C784;
–secondary: #F1F8E9;
–accent: #FF9800;
–accent-light: #FFB74D;
–text-dark: #263238;
–text-light: #546E7A;
–white: #FFFFFF;
–error: #EF5350;
–success: #66BB6A;
–card-default: #FFFFFF;
–card-selected: #C8E6C9;
–card-matched: #A5D6A7;
–card-error: #FFCDD2;
–shadow-sm: 0 2px 4px rgba(0,0,0,0.1);
–shadow-md: 0 4px 12px rgba(0,0,0,0.15);
–shadow-lg: 0 8px 24px rgba(0,0,0,0.2);
–radius-sm: 8px;
–radius-md: 12px;
–radius-lg: 20px;
}

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: ‘Nunito’, ‘Noto Sans SC’, sans-serif;
background: linear-gradient(135deg, #F1F8E9 0%, #E8F5E9 50%, #C8E6C9 100%);
min-height: 100vh;
color: var(–text-dark);
overflow-x: hidden;
}

/* Background Pattern */
body::before {
content: ”;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(circle at 20% 80%, rgba(76, 175, 80, 0.1) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(255, 152, 0, 0.08) 0%, transparent 50%),
radial-gradient(circle at 40% 40%, rgba(76, 175, 80, 0.05) 0%, transparent 30%);
pointer-events: none;
z-index: -1;
}

/* Header */
.header {
background: var(–white);
box-shadow: var(–shadow-sm);
padding: 15px 30px;
position: sticky;
top: 0;
z-index: 100;
}

.header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}

.logo {
display: flex;
align-items: center;
gap: 12px;
text-decoration: none;
color: var(–primary);
}

.logo-icon {
width: 45px;
height: 45px;
background: linear-gradient(135deg, var(–primary) 0%, var(–primary-light) 100%);
border-radius: var(–radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: white;
box-shadow: var(–shadow-sm);
}

.logo-text {
font-size: 22px;
font-weight: 800;
background: linear-gradient(135deg, var(–primary) 0%, var(–primary-dark) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}

.nav-links {
display: flex;
gap: 20px;
}

.nav-link {
padding: 8px 16px;
border-radius: var(–radius-sm);
text-decoration: none;
color: var(–text-dark);
font-weight: 600;
transition: all 0.3s ease;
}

.nav-link:hover {
background: var(–secondary);
color: var(–primary);
}

.nav-link.active {
background: var(–primary);
color: white;
}

.sound-toggle {
background: var(–secondary);
border: 2px solid var(–primary-light);
border-radius: var(–radius-sm);
padding: 8px 12px;
cursor: pointer;
font-size: 20px;
transition: all 0.3s ease;
display: flex;
align-items: center;
justify-content: center;
}

.sound-toggle:hover {
background: var(–primary);
border-color: var(–primary);
}

.sound-toggle:hover span {
transform: scale(1.1);
}

.sound-toggle.muted {
opacity: 0.6;
border-color: var(–text-light);
}

/* Dropdown Menu */
.nav-dropdown {
position: relative;
display: inline-block;
}

.dropdown-content {
display: none;
position: absolute;
background-color: var(–white);
min-width: 160px;
box-shadow: var(–shadow-lg);
z-index: 1000;
border-radius: var(–radius-sm);
overflow: hidden;
top: 100%;
left: 0;
}

.nav-dropdown:hover .dropdown-content {
display: block;
}

.dropdown-content a {
color: var(–text-dark);
padding: 12px 16px;
text-decoration: none;
display: block;
font-size: 14px;
transition: background 0.2s;
}

.dropdown-content a:hover {
background-color: var(–secondary);
color: var(–primary);
}

/* Main Container */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 30px 20px;
}

/* Page Transitions */
.page {
display: none;
animation: fadeIn 0.4s ease;
}

.page.active {
display: block;
}

@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}

/* Hero Section */
.hero {
text-align: center;
padding: 40px 0;
}

.hero-title {
font-size: 42px;
font-weight: 800;
color: var(–text-dark);
margin-bottom: 15px;
line-height: 1.2;
}

.hero-title span {
color: var(–primary);
}

.hero-subtitle {
font-size: 18px;
color: var(–text-light);
margin-bottom: 40px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
}

/* Grade Selection Grid */
.grade-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 25px;
margin-top: 30px;
}

.grade-card {
background: var(–white);
border-radius: var(–radius-lg);
padding: 30px;
box-shadow: var(–shadow-md);
cursor: pointer;
transition: all 0.3s ease;
border: 3px solid transparent;
position: relative;
overflow: hidden;
}

.grade-card::before {
content: ”;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 5px;
background: linear-gradient(90deg, var(–primary), var(–primary-light));
}

.grade-card:hover {
transform: translateY(-8px);
box-shadow: var(–shadow-lg);
border-color: var(–primary-light);
}

.grade-card-icon {
width: 70px;
height: 70px;
background: linear-gradient(135deg, var(–secondary) 0%, #DCEDC8 100%);
border-radius: var(–radius-md);
display: flex;
align-items: center;
justify-content: center;
font-size: 36px;
margin-bottom: 20px;
}

.grade-card-title {
font-size: 22px;
font-weight: 700;
color: var(–text-dark);
margin-bottom: 10px;
}

.grade-card-desc {
font-size: 14px;
color: var(–text-light);
line-height: 1.6;
}

.grade-card-badge {
position: absolute;
top: 15px;
right: 15px;
background: var(–accent);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
}

/* Book Selection */
.book-section {
background: var(–white);
border-radius: var(–radius-lg);
padding: 30px;
box-shadow: var(–shadow-md);
margin-top: 30px;
}

{
font-size: 24 .section-titlepx;
font-weight: 700;
color: var(–text-dark);
margin-bottom: 25px;
display: flex;
align-items: center;
gap: 12px;
}

.section-title::before {
content: ”;
width: 5px;
height: 28px;
background: var(–primary);
border-radius: 3px;
}

.book-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}

.book-card {
background: var(–secondary);
border-radius: var(–radius-md);
padding: 20px;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
text-align: center;
}

.book-card:hover {
background: var(–primary-light);
color: white;
border-color: var(–primary);
}

.book-card.selected {
background: var(–primary);
color: white;
border-color: var(–primary-dark);
}

.book-card-title {
font-weight: 600;
font-size: 15px;
}

/* Game Setup */
.game-setup {
background: var(–white);
border-radius: var(–radius-lg);
padding: 30px;
box-shadow: var(–shadow-md);
margin-top: 30px;
}

.setup-row {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 25px;
flex-wrap: wrap;
}

.setup-label {
font-weight: 600;
color: var(–text-dark);
min-width: 100px;
}

.difficulty-btns, .mode-btns {
display: flex;
gap: 10px;
}

.difficulty-btn, .mode-btn {
padding: 10px 20px;
border: 2px solid var(–primary-light);
background: var(–white);
border-radius: var(–radius-sm);
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
font-size: 14px;
}

.difficulty-btn:hover, .mode-btn:hover {
background: var(–secondary);
}

.difficulty-btn.active, .mode-btn.active {
background: var(–primary);
color: white;
border-color: var(–primary);
}

.start-btn {
width: 100%;
padding: 18px 40px;
background: linear-gradient(135deg, var(–primary) 0%, var(–primary-dark) 100%);
color: white;
border: none;
border-radius: var(–radius-md);
font-size: 20px;
font-weight: 700;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: var(–shadow-md);
margin-top: 20px;
}

.start-btn:hover {
transform: translateY(-3px);
box-shadow: var(–shadow-lg);
}

/* Game Interface */
.game-header {
background: var(–white);
border-radius: var(–radius-lg);
padding: 20px 30px;
box-shadow: var(–shadow-md);
margin-bottom: 25px;
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
}

.game-info {
display: flex;
align-items: center;
gap: 30px;
}

.game-stat {
display: flex;
align-items: center;
gap: 8px;
}

.game-stat-icon {
width: 36px;
height: 36px;
background: var(–secondary);
border-radius: var(–radius-sm);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}

.game-stat-label {
font-size: 12px;
color: var(–text-light);
}

.game-stat-value {
font-size: 20px;
font-weight: 700;
color: var(–text-dark);
}

.timer-warning {
color: var(–error) !important;
animation: pulse 0.5s infinite;
}

@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}

.game-controls {
display: flex;
gap: 10px;
}

.control-btn {
padding: 10px 18px;
border: 2px solid var(–primary-light);
background: var(–white);
border-radius: var(–radius-sm);
cursor: pointer;
font-weight: 600;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 6px;
}

.control-btn:hover {
background: var(–primary);
color: white;
border-color: var(–primary);
}

.control-btn.hint {
border-color: var(–accent);
color: var(–accent);
}

.control-btn.hint:hover {
background: var(–accent);
color: white;
}

/* Game Grid */
.game-container {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 30px;
flex-wrap: wrap;
}

.game-grid-wrapper {
background: var(–white);
border-radius: var(–radius-lg);
padding: 25px;
box-shadow: var(–shadow-lg);
position: relative;
}

.game-grid {
display: grid;
gap: 8px;
position: relative;
}

.game-tile {
width: 80px;
height: 70px;
background: linear-gradient(135deg, var(–white) 0%, var(–secondary) 100%);
border: 2px solid #E0E0E0;
border-radius: var(–radius-sm);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s ease;
font-size: 14px;
font-weight: 600;
text-align: center;
padding: 5px;
user-select: none;
position: relative;
}

.game-tile:hover:not(.matched):not(.empty) {
transform: translateY(-3px);
box-shadow: var(–shadow-sm);
border-color: var(–primary);
}

.game-tile.selected {
background: var(–card-selected);
border-color: var(–primary);
box-shadow: 0 0 0 3px rgba(76, 175, 80, 0.3);
}

.game-tile.matched {
background: var(–card-matched);
border-color: var(–success);
opacity: 0;
pointer-events: none;
animation: matchFade 0.4s ease forwards;
}

@keyframes matchFade {
0% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.1); }
100% { opacity: 0; transform: scale(0.8); }
}

.game-tile.error {
background: var(–card-error);
border-color: var(–error);
animation: shake 0.4s ease;
}

@keyframes shake {
0%, 100% { transform: translateX(0); }
20% { transform: translateX(-8px); }
40% { transform: translateX(8px); }
60% { transform: translateX(-8px); }
80% { transform: translateX(8px); }
}

.game-tile.empty {
background: transparent;
border-color: transparent;
pointer-events: none;
}

.game-tile.english {
color: var(–primary-dark);
}

.game-tile.chinese {
color: var(–accent);
}

/* Connection Line SVG */
.connection-svg {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}

.connection-line {
stroke: var(–primary);
stroke-width: 4;
fill: none;
stroke-linecap: round;
stroke-linejoin: round;
filter: drop-shadow(0 2px 3px rgba(76, 175, 80, 0.4));
}

.connection-line-anim {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: drawLine 0.4s ease forwards;
}

@keyframes drawLine {
to { stroke-dashoffset: 0; }
}

/* Game Sidebar */
.game-sidebar {
width: 280px;
display: flex;
flex-direction: column;
gap: 20px;
}

.sidebar-card {
background: var(–white);
border-radius: var(–radius-md);
padding: 20px;
box-shadow: var(–shadow-md);
}

.sidebar-title {
font-size: 16px;
font-weight: 700;
color: var(–text-dark);
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
}

.word-list {
max-height: 200px;
overflow-y: auto;
}

.word-item {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #eee;
font-size: 13px;
}

.word-item:last-child {
border-bottom: none;
}

.word-english {
color: var(–primary-dark);
font-weight: 600;
}

.word-chinese {
color: var(–text-light);
}

.word-item.matched .word-english,
.word-item.matched .word-chinese {
color: var(–success);
text-decoration: line-through;
}

/* Progress Bar */
.progress-container {
margin-top: 15px;
}

.progress-bar {
width: 100%;
height: 10px;
background: var(–secondary);
border-radius: 10px;
overflow: hidden;
}

.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(–primary) 0%, var(–primary-light) 100%);
border-radius: 10px;
transition: width 0.4s ease;
}

.progress-text {
font-size: 12px;
color: var(–text-light);
margin-top: 8px;
text-align: center;
}

/* Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
}

.modal-overlay.active {
opacity: 1;
visibility: visible;
}

.modal {
background: var(–white);
border-radius: var(–radius-lg);
padding: 40px;
text-align: center;
max-width: 400px;
width: 90%;
transform: scale(0.8);
transition: all 0.3s ease;
box-shadow: var(–shadow-lg);
}

.modal-overlay.active .modal {
transform: scale(1);
}

.modal-icon {
width: 80px;
height: 80px;
background: linear-gradient(135deg, var(–primary) 0%, var(–primary-light) 100%);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40px;
margin: 0 auto 20px;
color: white;
}

.modal-icon.fail {
background: linear-gradient(135deg, var(–error) 0%, #FF8A80 100%);
}

.modal-title {
font-size: 28px;
font-weight: 700;
color: var(–text-dark);
margin-bottom: 15px;
}

.modal-desc {
font-size: 16px;
color: var(–text-light);
margin-bottom: 25px;
line-height: 1.6;
}

.modal-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-bottom: 30px;
}

.modal-stat {
text-align: center;
}

.modal-stat-value {
font-size: 32px;
font-weight: 800;
color: var(–primary);
}

.modal-stat-label {
font-size: 12px;
color: var(–text-light);
}

.stars {
font-size: 36px;
margin-bottom: 20px;
}

.star {
color: #ddd;
margin: 0 3px;
}

.star.filled {
color: var(–accent);
}

.modal-btns {
display: flex;
gap: 15px;
justify-content: center;
}

.modal-btn {
padding: 12px 30px;
border: none;
border-radius: var(–radius-sm);
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}

.modal-btn.primary {
background: var(–primary);
color: white;
}

.modal-btn.primary:hover {
background: var(–primary-dark);
}

.modal-btn.secondary {
background: var(–secondary);
color: var(–text-dark);
}

.modal-btn.secondary:hover {
background: #DCEDC8;
}

/* Back Button */
.back-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 10px 20px;
background: var(–white);
border: 2px solid var(–primary-light);
border-radius: var(–radius-sm);
cursor: pointer;
font-weight: 600;
color: var(–text-dark);
transition: all 0.3s ease;
margin-bottom: 25px;
text-decoration: none;
}

.back-btn:hover {
background: var(–primary);
color: white;
border-color: var(–primary);
}

/* Footer */
.footer {
text-align: center;
padding: 30px;
color: var(–text-light);
font-size: 14px;
}

/* Responsive */
@media (max-width: 900px) {
.game-sidebar {
width: 100%;
flex-direction: row;
flex-wrap: wrap;
}

.sidebar-card {
flex: 1;
min-width: 200px;
}

.game-grid {
justify-content: center;
}
}

@media (max-width: 600px) {
.hero-title {
font-size: 28px;
}

.grade-grid {
grid-template-columns: 1fr;
}

.game-tile {
width: 65px;
height: 55px;
font-size: 11px;
}

.game-header {
flex-direction: column;
align-items: stretch;
}

.game-info {
justify-content: space-around;
}
}

/* Hint Animation */
@keyframes hint-glow {
0%, 100% { box-shadow: 0 0 5px var(–accent); }
50% { box-shadow: 0 0 20px var(–accent), 0 0 30px var(–accent-light); }
}

.game-tile.hint {
animation: hint-glow 0.8s ease infinite;
border-color: var(–accent);
}

/* Particles */
.particle {
position: absolute;
width: 10px;
height: 10px;
border-radius: 50%;
pointer-events: none;
animation: particle-fly 0.8s ease-out forwards;
}

@keyframes particle-fly {
0% { opacity: 1; transform: scale(1); }
100% { opacity: 0; transform: scale(0) translateY(-50px); }
}
</style>
</head>
<body>
<!– Header –>
<header class=”header”>
<div class=”header-content”>
<a href=”#” class=”logo” onclick=”showPage(‘home’)”>
<div class=”logo-icon”>📚</div>
<span class=”logo-text”>英语学习乐园</span>
</a>
<nav class=”nav-links”>
<a href=”#” class=”nav-link active” data-page=”home”>首页</a>
<div class=”nav-dropdown”>
<a href=”#” class=”nav-link”>英语游戏 ▾</a>
<div class=”dropdown-content”>
<a href=”#” onclick=”showPage(‘home’)”>牛津英语连连看</a>
<a href=”#”>英语消消乐</a>
<a href=”#”>单词闯关</a>
<a href=”#”>听力练习</a>
</div>
</div>
<div class=”nav-dropdown”>
<a href=”#” class=”nav-link”>英文小说 ▾</a>
<div class=”dropdown-content”>
<a href=”#”>经典英文小说</a>
<a href=”#”>双语小说</a>
<a href=”#”>英文短篇小说</a>
<a href=”#”>英文科幻小说</a>
<a href=”#”>儿童英文小说</a>
<a href=”#”>英文宗教小说</a>
</div>
</div>
<div class=”nav-dropdown”>
<a href=”#” class=”nav-link”>英语教材 ▾</a>
<div class=”dropdown-content”>
<a href=”#” onclick=”showPage(‘home’)”>牛津英语</a>
<a href=”#”>新概念英语</a>
<a href=”#”>走遍美国</a>
<a href=”#”>赖世雄英语</a>
</div>
</div>
<div class=”nav-dropdown”>
<a href=”#” class=”nav-link”>听力教程 ▾</a>
<div class=”dropdown-content”>
<a href=”#”>VOA慢速英语</a>
<a href=”#”>VOA常速英语</a>
<a href=”#”>BBC英语</a>
<a href=”#”>CNN英语</a>
</div>
</div>
<div class=”nav-dropdown”>
<a href=”#” class=”nav-link”>英语视频 ▾</a>
<div class=”dropdown-content”>
<a href=”#”>英语电影</a>
<a href=”#”>英语纪录片</a>
<a href=”#”>英语演讲</a>
<a href=”#”>TED演讲</a>
</div>
</div>
<div class=”nav-dropdown”>
<a href=”#” class=”nav-link”>传记励志 ▾</a>
<div class=”dropdown-content”>
<a href=”#”>英文名人传记</a>
<a href=”#”>英文励志小说</a>
<a href=”#”>成功学</a>
</div>
</div>
<a href=”#” class=”nav-link” onclick=”showHowToPlay()”>游戏说明</a>
</nav>
<button class=”sound-toggle” id=”sound-toggle” onclick=”toggleSound()”>
<span id=”sound-icon”>🔊</span>
</button>
</div>
</header>

<!– Main Container –>
<main class=”container”>
<!– Home Page –>
<div id=”home-page” class=”page active”>
<div class=”hero”>
<h1 class=”hero-title”>在游戏中掌握<span>英语单词</span></h1>
<p class=”hero-subtitle”>选择你的年级,开始牛津英语单词连连看挑战,在游戏中轻松记住每一个单词!</p>
</div>

<div class=”grade-grid”>
<div class=”grade-card” onclick=”selectGrade(‘primary’, 1)”>
<div class=”grade-card-icon”>🎒</div>
<h3 class=”grade-card-title”>小学一年级</h3>
<p class=”grade-card-desc”>牛津英语一年级上下册,适合英语启蒙学习</p>
<span class=”grade-card-badge”>入门</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘primary’, 2)”>
<div class=”grade-card-icon”>📖</div>
<h3 class=”grade-card-title”>小学二年级</h3>
<p class=”grade-card-desc”>牛津英语二年级上下册,巩固基础词汇</p>
<span class=”grade-card-badge”>基础</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘primary’, 3)”>
<div class=”grade-card-icon”>✏️</div>
<h3 class=”grade-card-title”>小学三年级</h3>
<p class=”grade-card-desc”>牛津英语三年级上下册,系统学习单词</p>
<span class=”grade-card-badge”>进阶</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘primary’, 4)”>
<div class=”grade-card-icon”>📝</div>
<h3 class=”grade-card-title”>小学四年级</h3>
<p class=”grade-card-desc”>牛津英语四年级上下册,提升词汇量</p>
<span class=”grade-card-badge”>提升</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘primary’, 5)”>
<div class=”grade-card-icon”>📚</div>
<h3 class=”grade-card-title”>小学五年级</h3>
<p class=”grade-card-desc”>牛津英语五年级上下册,扩大词汇范围</p>
<span class=”grade-card-badge”>扩展</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘primary’, 6)”>
<div class=”grade-card-icon”>🎓</div>
<h3 class=”grade-card-title”>小学六年级</h3>
<p class=”grade-card-desc”>牛津英语六年级上下册,小学毕业冲刺</p>
<span class=”grade-card-badge”>冲刺</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘junior’, 7)”>
<div class=”grade-card-icon”>🏫</div>
<h3 class=”grade-card-title”>初中一年级</h3>
<p class=”grade-card-desc”>牛津英语七年级,系统初中英语学习</p>
<span class=”grade-card-badge”>初中</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘junior’, 8)”>
<div class=”grade-card-icon”>🏫</div>
<h3 class=”grade-card-title”>初中二年级</h3>
<p class=”grade-card-desc”>牛津英语八年级,深化语法词汇</p>
<span class=”grade-card-badge”>初中</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘junior’, 9)”>
<div class=”grade-card-icon”>🏫</div>
<h3 class=”grade-card-title”>初中三年级</h3>
<p class=”grade-card-desc”>牛津英语九年级,中考复习准备</p>
<span class=”grade-card-badge”>中考</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘senior’, 10)”>
<div class=”grade-card-icon”>🌟</div>
<h3 class=”grade-card-title”>高中一年级</h3>
<p class=”grade-card-desc”>牛津英语高一,全新高中英语学习</p>
<span class=”grade-card-badge”>高中</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘senior’, 11)”>
<div class=”grade-card-icon”>🌟</div>
<h3 class=”grade-card-title”>高中二年级</h3>
<p class=”grade-card-desc”>牛津英语高二,深化英语能力</p>
<span class=”grade-card-badge”>高中</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘senior’, 12)”>
<div class=”grade-card-icon”>🎯</div>
<h3 class=”grade-card-title”>高中三年级</h3>
<p class=”grade-card-desc”>牛津英语高三,高考冲刺复习</p>
<span class=”grade-card-badge”>高考</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘college’, 1)”>
<div class=”grade-card-icon”>🎓</div>
<h3 class=”grade-card-title”>大学一年级</h3>
<p class=”grade-card-desc”>大学英语一年级,开始大学英语学习</p>
<span class=”grade-card-badge”>大学</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘college’, 2)”>
<div class=”grade-card-icon”>🎓</div>
<h3 class=”grade-card-title”>大学二年级</h3>
<p class=”grade-card-desc”>大学英语二年级,提升英语能力</p>
<span class=”grade-card-badge”>大学</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘college’, 3)”>
<div class=”grade-card-icon”>🎓</div>
<h3 class=”grade-card-title”>大学三年级</h3>
<p class=”grade-card-desc”>大学英语三年级,专业英语学习</p>
<span class=”grade-card-badge”>大学</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘college’, 4)”>
<div class=”grade-card-icon”>🎓</div>
<h3 class=”grade-card-title”>大学四年级</h3>
<p class=”grade-card-desc”>大学英语四年级,毕业论文英语</p>
<span class=”grade-card-badge”>大学</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘graduate’, 1)”>
<div class=”grade-card-icon”>📚</div>
<h3 class=”grade-card-title”>研究生一年级</h3>
<p class=”grade-card-desc”>研究生英语一年级,学术英语入门</p>
<span class=”grade-card-badge”>研究生</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘graduate’, 2)”>
<div class=”grade-card-icon”>📚</div>
<h3 class=”grade-card-title”>研究生二年级</h3>
<p class=”grade-card-desc”>研究生英语二年级,研究方法英语</p>
<span class=”grade-card-badge”>研究生</span>
</div>
<div class=”grade-card” onclick=”selectGrade(‘graduate’, 3)”>
<div class=”grade-card-icon”>📚</div>
<h3 class=”grade-card-title”>研究生三年级</h3>
<p class=”grade-card-desc”>研究生英语三年级,论文答辩英语</p>
<span class=”grade-card-badge”>研究生</span>
</div>
</div>
</div>

<!– Book Selection Page –>
<div id=”book-page” class=”page”>
<a href=”#” class=”back-btn” onclick=”showPage(‘home’)”>
← 返回年级选择
</a>

<div class=”book-section”>
<h2 class=”section-title” id=”book-section-title”>选择教材版本</h2>
<div class=”book-grid” id=”book-grid”>
<!– Books will be populated by JS –>
</div>
</div>

<div class=”game-setup”>
<h2 class=”section-title”>游戏设置</h2>

<div class=”setup-row”>
<span class=”setup-label”>选择难度:</span>
<div class=”difficulty-btns”>
<button class=”difficulty-btn” data-difficulty=”easy” onclick=”selectDifficulty(‘easy’)”>简单</button>
<button class=”difficulty-btn active” data-difficulty=”medium” onclick=”selectDifficulty(‘medium’)”>中等</button>
<button class=”difficulty-btn” data-difficulty=”hard” onclick=”selectDifficulty(‘hard’)”>困难</button>
</div>
</div>

<div class=”setup-row”>
<span class=”setup-label”>游戏模式:</span>
<div class=”mode-btns”>
<button class=”mode-btn active” data-mode=”learn” onclick=”selectMode(‘learn’)”>学习模式</button>
<button class=”mode-btn” data-mode=”review” onclick=”selectMode(‘review’)”>复习模式</button>
<button class=”mode-btn” data-mode=”test” onclick=”selectMode(‘test’)”>测试模式</button>
</div>
</div>

<button class=”start-btn” onclick=”startGame()”>
🚀 开始游戏
</button>
</div>
</div>

<!– Game Page –>
<div id=”game-page” class=”page”>
<a href=”#” class=”back-btn” onclick=”quitGame()”>
← 退出游戏
</a>

<div class=”game-header”>
<div class=”game-info”>
<div class=”game-stat”>
<div class=”game-stat-icon”>📖</div>
<div>
<div class=”game-stat-label”>当前词汇</div>
<div class=”game-stat-value” id=”current-book”>-</div>
</div>
</div>
<div class=”game-stat”>
<div class=”game-stat-icon”>⏱️</div>
<div>
<div class=”game-stat-label”>剩余时间</div>
<div class=”game-stat-value” id=”timer”>00:00</div>
</div>
</div>
<div class=”game-stat”>
<div class=”game-stat-icon”>⭐</div>
<div>
<div class=”game-stat-label”>当前得分</div>
<div class=”game-stat-value” id=”score”>0</div>
</div>
</div>
<div class=”game-stat”>
<div class=”game-stat-icon”>✅</div>
<div>
<div class=”game-stat-label”>已匹配</div>
<div class=”game-stat-value” id=”matched”>0 / 0</div>
</div>
</div>
</div>
<div class=”game-controls”>
<button class=”control-btn hint” onclick=”useHint()” id=”hint-btn”>
💡 提示 (<span id=”hints-left”>3</span>)
</button>
<button class=”control-btn” onclick=”shuffleTiles()”>
🔀 洗牌
</button>
<button class=”control-btn” onclick=”pauseGame()”>
⏸️ 暂停
</button>
</div>
</div>

<div class=”game-container”>
<div class=”game-grid-wrapper”>
<div class=”game-grid” id=”game-grid”>
<!– Game tiles will be generated by JS –>
</div>
<svg class=”connection-svg” id=”connection-svg”>
<!– Connection lines will be drawn here –>
</svg>
</div>

<div class=”game-sidebar”>
<div class=”sidebar-card”>
<h3 class=”sidebar-title”>📝 词汇表</h3>
<div class=”word-list” id=”word-list”>
<!– Word list will be populated by JS –>
</div>
<div class=”progress-container”>
<div class=”progress-bar”>
<div class=”progress-fill” id=”progress-fill” style=”width: 0%”></div>
</div>
<div class=”progress-text” id=”progress-text”>0% 完成</div>
</div>
</div>

<div class=”sidebar-card”>
<h3 class=”sidebar-title”>🎯 游戏目标</h3>
<p style=”font-size: 13px; color: var(–text-light); line-height: 1.6;”>
找出英文单词和其中文含义的对应关系,点击配对即可!先点击英文单词,再点击对应的中文翻译。
</p>
</div>
</div>
</div>
</div>
</main>

<!– Victory Modal –>
<div class=”modal-overlay” id=”victory-modal”>
<div class=”modal”>
<div class=”modal-icon”>🎉</div>
<h2 class=”modal-title”>恭喜通关!</h2>
<p class=”modal-desc”>你太棒了!成功完成了本单元的所有词汇!</p>
<div class=”stars” id=”stars”>
<span class=”star filled”>★</span>
<span class=”star filled”>★</span>
<span class=”star filled”>★</span>
</div>
<div class=”modal-stats”>
<div class=”modal-stat”>
<div class=”modal-stat-value” id=”final-score”>0</div>
<div class=”modal-stat-label”>最终得分</div>
</div>
<div class=”modal-stat”>
<div class=”modal-stat-value” id=”final-time”>00:00</div>
<div class=”modal-stat-label”>用时</div>
</div>
</div>
<div class=”modal-btns”>
<button class=”modal-btn secondary” onclick=”restartGame()”>再玩一次</button>
<button class=”modal-btn primary” onclick=”nextLevel()”>下一关</button>
</div>
</div>
</div>

<!– Game Over Modal –>
<div class=”modal-overlay” id=”gameover-modal”>
<div class=”modal”>
<div class=”modal-icon fail”>😢</div>
<h2 class=”modal-title”>时间到!</h2>
<p class=”modal-desc”>很遗憾,时间用完了。再试一次吧!</p>
<div class=”modal-stats”>
<div class=”modal-stat”>
<div class=”modal-stat-value” id=”gameover-score”>0</div>
<div class=”modal-stat-label”>得分</div>
</div>
<div class=”modal-stat”>
<div class=”modal-stat-value” id=”gameover-matched”>0</div>
<div class=”modal-stat-label”>匹配对数</div>
</div>
</div>
<div class=”modal-btns”>
<button class=”modal-btn primary” onclick=”restartGame()”>重新挑战</button>
</div>
</div>
</div>

<!– Pause Modal –>
<div class=”modal-overlay” id=”pause-modal”>
<div class=”modal”>
<div class=”modal-icon”>⏸️</div>
<h2 class=”modal-title”>游戏暂停</h2>
<p class=”modal-desc”>休息一下,准备好了继续吗?</p>
<div class=”modal-btns”>
<button class=”modal-btn primary” onclick=”resumeGame()”>继续游戏</button>
</div>
</div>
</div>

<!– How to Play Modal –>
<div class=”modal-overlay” id=”howto-modal”>
<div class=”modal”>
<div class=”modal-icon”>📖</div>
<h2 class=”modal-title”>游戏说明</h2>
<div class=”modal-desc” style=”text-align: left;”>
<p style=”margin-bottom: 15px;”><strong>🎯 目标:</strong></p>
<p style=”margin-bottom: 15px;”>在规定时间内,找出英文单词和其中文含义的对应关系,点击配对即可!</p>
<p style=”margin-bottom: 15px;”><strong>🔗 玩法:</strong></p>
<p style=”margin-bottom: 10px;”>• 先点击一个英文单词</p>
<p style=”margin-bottom: 10px;”>• 再点击对应的中文翻译</p>
<p style=”margin-bottom: 10px;”>• 配对成功即可消除卡片</p>
<p style=”margin-bottom: 15px;”>• 配对错误会显示抖动提示</p>
<p style=”margin-bottom: 15px;”><strong>💡 提示:</strong></p>
<p>点击”提示”按钮可查看一组匹配的卡片(共3次)</p>
</div>
<div class=”modal-btns”>
<button class=”modal-btn primary” onclick=”closeHowToPlay()”>明白了!</button>
</div>
</div>
</div>

<footer class=”footer”>
<p>© 2026 牛津英语连连看 – 在游戏中掌握英语词汇</p>
</footer>

<script>
// ==================== VOCABULARY DATA ====================
const vocabularyData = {
primary: {
1: {
name: “牛津英语一年级”,
units: [
{ name: “第一单元”, words: { “hello”: “你好”, “good”: “好”, “morning”: “早晨”, “bye”: “再见”, “no”: “不”, “thank”: “谢谢”, “sorry”: “对不起”, “OK”: “好的”, “I”: “我”, “am”: “是”, “you”: “你”, “are”: “是”, “what”: “什么”, “name”: “名字”, “my”: “我的” } },
{ name: “第二单元”, words: { “bird”: “鸟”, “dog”: “狗”, “cat”: “猫”, “fish”: “鱼”, “duck”: “鸭子”, “monkey”: “猴子”, “rabbit”: “兔子”, “pig”: “猪”, “elephant”: “大象”, “lion”: “狮子”, “bear”: “熊”, “tiger”: “老虎”, “zoo”: “动物园”, “animal”: “动物”, “cute”: “可爱的”, “big”: “大的” } }
]
},
2: {
name: “牛津英语二年级”,
units: [
{ name: “第一单元”, words: { “apple”: “苹果”, “banana”: “香蕉”, “orange”: “橙子”, “milk”: “牛奶”, “bread”: “面包”, “egg”: “鸡蛋”, “rice”: “米饭”, “water”: “水”, “juice”: “果汁”, “cake”: “蛋糕”, “fish”: “鱼”, “meat”: “肉”, “hungry”: “饥饿的”, “thirsty”: “口渴的” } },
{ name: “第二单元”, words: { “red”: “红色”, “blue”: “蓝色”, “yellow”: “黄色”, “green”: “绿色”, “purple”: “紫色”, “pink”: “粉色”, “white”: “白色”, “black”: “黑色”, “orange”: “橙色”, “brown”: “棕色”, “color”: “颜色”, “look”: “看”, “see”: “看见”, “what”: “什么” } }
]
},
3: {
name: “牛津英语三年级”,
units: [
{ name: “第一单元”, words: { “school”: “学校”, “classroom”: “教室”, “door”: “门”, “window”: “窗户”, “desk”: “书桌”, “chair”: “椅子”, “blackboard”: “黑板”, “book”: “书”, “bag”: “包”, “pen”: “钢笔”, “pencil”: “铅笔”, “ruler”: “尺子”, “eraser”: “橡皮”, “crayon”: “蜡笔” } },
{ name: “第二单元”, words: { “family”: “家庭”, “father”: “父亲”, “mother”: “母亲”, “brother”: “兄弟”, “sister”: “姐妹”, “grandfather”: “祖父”, “grandmother”: “祖母”, “doctor”: “医生”, “nurse”: “护士”, “teacher”: “教师”, “student”: “学生”, “friend”: “朋友”, “boy”: “男孩”, “girl”: “女孩” } },
{ name: “第三单元”, words: { “one”: “一”, “two”: “二”, “three”: “三”, “four”: “四”, “five”: “五”, “six”: “六”, “seven”: “七”, “eight”: “八”, “nine”: “九”, “ten”: “十”, “how”: “怎样”, “many”: “许多”, “old”: “年长的”, “years”: “年” } }
]
},
4: {
name: “牛津英语四年级”,
units: [
{ name: “第一单元”, words: { “subject”: “学科”, “Chinese”: “语文”, “English”: “英语”, “maths”: “数学”, “music”: “音乐”, “art”: “美术”, “PE”: “体育”, “science”: “科学”, “like”: “喜欢”, “favorite”: “最喜欢的”, “subject”: “科目”, “interesting”: “有趣的”, “difficult”: “困难的”, “easy”: “容易的” } },
{ name: “第二单元”, words: { “day”: “天”, “Monday”: “星期一”, “Tuesday”: “星期二”, “Wednesday”: “星期三”, “Thursday”: “星期四”, “Friday”: “星期五”, “Saturday”: “星期六”, “Sunday”: “星期日”, “week”: “周”, “today”: “今天”, “tomorrow”: “明天”, “schedule”: “日程表” } },
{ name: “第三单元”, words: { “weather”: “天气”, “sunny”: “晴朗的”, “rainy”: “下雨的”, “cloudy”: “多云的”, “windy”: “有风的”, “snowy”: “下雪的”, “hot”: “热的”, “cold”: “冷的”, “warm”: “温暖的”, “cool”: “凉爽的”, “temperature”: “温度”, “degree”: “度” } }
]
},
5: {
name: “牛津英语五年级”,
units: [
{ name: “第一单元”, words: { “season”: “季节”, “spring”: “春天”, “summer”: “夏天”, “autumn”: “秋天”, “winter”: “冬天”, “weather”: “天气”, “plant”: “植物”, “grow”: “生长”, “leaf”: “叶子”, “flower”: “花”, “tree”: “树”, “grass”: “草”, “green”: “绿色的” } },
{ name: “第二单元”, words: { “room”: “房间”, “bedroom”: “卧室”, “living room”: “客厅”, “kitchen”: “厨房”, “bathroom”: “浴室”, “garden”: “花园”, “study”: “书房”, “bed”: “床”, “table”: “桌子”, “sofa”: “沙发”, “chair”: “椅子”, “TV”: “电视”, “computer”: “电脑” } },
{ name: “第三单元”, words: { “hobby”: “爱好”, “reading”: “阅读”, “painting”: “绘画”, “dancing”: “跳舞”, “singing”: “唱歌”, “swimming”: “游泳”, “running”: “跑步”, “playing”: “玩”, “watching”: “看”, “listening”: “听”, “often”: “经常”, “sometimes”: “有时” } }
]
},
6: {
name: “牛津英语六年级”,
units: [
{ name: “第一单元”, words: { “dream”: “梦想”, “future”: “未来”, “become”: “成为”, “engineer”: “工程师”, “doctor”: “医生”, “teacher”: “教师”, “artist”: “艺术家”, “scientist”: “科学家”, “pilot”: “飞行员”, “astronaut”: “宇航员”, “want”: “想要”, “would”: “将会”, “maybe”: “也许” } },
{ name: “第二单元”, words: { “different”: “不同的”, “same”: “相同的”, “country”: “国家”, “China”: “中国”, “UK”: “英国”, “USA”: “美国”, “Australia”: “澳大利亚”, “Canada”: “加拿大”, “Japan”: “日本”, “live”: “居住”, “come”: “来”, “from”: “来自” } },
{ name: “第三单元”, words: { “diet”: “饮食”, “healthy”: “健康的”, “food”: “食物”, “vegetable”: “蔬菜”, “fruit”: “水果”, “meat”: “肉”, “fish”: “鱼”, “rice”: “米饭”, “bread”: “面包”, “milk”: “牛奶”, “water”: “水”, “juice”: “果汁” } }
]
}
},
junior: {
7: {
name: “牛津英语七年级”,
units: [
{ name: “第一单元”, words: { “introduce”: “介绍”, “myself”: “我自己”, “name”: “名字”, “age”: “年龄”, “school”: “学校”, “grade”: “年级”, “class”: “班级”, “telephone”: “电话”, “email”: “电子邮件”, “friend”: “朋友”, “family”: “家庭”, “hobby”: “爱好” } },
{ name: “第二单元”, words: { “daily”: “日常的”, “life”: “生活”, “wake”: “醒来”, “up”: “起床”, “brush”: “刷”, “teeth”: “牙齿”, “breakfast”: “早餐”, “lunch”: “午餐”, “dinner”: “晚餐”, “school”: “学校”, “homework”: “家庭作业”, “sleep”: “睡觉” } },
{ name: “第三单元”, words: { “sports”: “运动”, “football”: “足球”, “basketball”: “篮球”, “volleyball”: “排球”, “tennis”: “网球”, “swim”: “游泳”, “run”: “跑”, “jump”: “跳”, “play”: “玩”, “match”: “比赛”, “team”: “队”, “player”: “球员” } }
]
},
8: {
name: “牛津英语八年级”,
units: [
{ name: “第一单元”, words: { “past”: “过去的”, “time”: “时间”, “remember”: “记得”, “childhood”: “童年”, “memory”: “记忆”, “primary”: “初级的”, “school”: “学校”, “teacher”: “教师”, “friend”: “朋友”, “play”: “玩”, “study”: “学习”, “happy”: “快乐的” } },
{ name: “第二单元”, words: { “rules”: “规则”, “should”: “应该”, “must”: “必须”, “mustn’t”: “禁止”, “can”: “可以”, “can’t”: “不可以”, “library”: “图书馆”, “classroom”: “教室”, “corridor”: “走廊”, “canteen”: “食堂”, “quiet”: “安静的”, “keep”: “保持” } },
{ name: “第三单元”, words: { “environment”: “环境”, “protect”: “保护”, “earth”: “地球”, “pollution”: “污染”, “plastic”: “塑料”, “waste”: “垃圾”, “recycle”: “回收”, “energy”: “能源”, “save”: “节约”, “water”: “水”, “electricity”: “电”, “paper”: “纸” } }
]
},
9: {
name: “牛津英语九年级”,
units: [
{ name: “第一单元”, words: { “knowledge”: “知识”, “learn”: “学习”, “study”: “学习”, “practice”: “练习”, “question”: “问题”, “answer”: “回答”, “understand”: “理解”, “meaning”: “意义”, “word”: “单词”, “sentence”: “句子”, “grammar”: “语法”, “vocabulary”: “词汇” } },
{ name: “第二单元”, words: { “believe”: “相信”, “fact”: “事实”, “truth”: “真相”, “story”: “故事”, “news”: “新闻”, “report”: “报道”, “article”: “文章”, “newspaper”: “报纸”, “magazine”: “杂志”, “website”: “网站”, “information”: “信息”, “world”: “世界” } },
{ name: “第三单元”, words: { “culture”: “文化”, “country”: “国家”, “tradition”: “传统”, “festival”: “节日”, “celebrate”: “庆祝”, “custom”: “习俗”, “food”: “食物”, “family”: “家庭”, “meeting”: “聚会”, “important”: “重要的”, “special”: “特别的”, “different”: “不同的” } }
]
}
},
senior: {
10: {
name: “牛津英语高一”,
units: [
{ name: “第一单元”, words: { “experience”: “经历/经验”, “impression”: “印象”, “memory”: “记忆”, “moment”: “时刻”, “feeling”: “感觉”, “emotion”: “情感”, “thought”: “想法”, “idea”: “主意”, “opinion”: “观点”, “attitude”: “态度”, “view”: “看法” } },
{ name: “第二单元”, words: { “relationship”: “关系”, “family”: “家庭”, “friend”: “朋友”, “love”: “爱”, “care”: “关心”, “support”: “支持”, “trust”: “信任”, “respect”: “尊重”, “understand”: “理解”, “communicate”: “交流”, “share”: “分享”, “help”: “帮助” } },
{ name: “第三单元”, words: { “travel”: “旅行”, “journey”: “旅程”, “trip”: “旅行”, “tour”: “旅游”, “visit”: “参观”, “place”: “地方”, “country”: “国家”, “city”: “城市”, “beautiful”: “美丽的”, “interesting”: “有趣的”, “wonderful”: “精彩的”, “amazing”: “惊人的” } }
]
},
11: {
name: “牛津英语高二”,
units: [
{ name: “第一单元”, words: { “science”: “科学”, “technology”: “技术”, “computer”: “电脑”, “internet”: “互联网”, “information”: “信息”, “develop”: “发展”, “discover”: “发现”, “invent”: “发明”, “research”: “研究”, “experiment”: “实验”, “result”: “结果”, “theory”: “理论” } },
{ name: “第二单元”, words: { “success”: “成功”, “achieve”: “实现/达到”, “goal”: “目标”, “dream”: “梦想”, “effort”: “努力”, “hard”: “努力地”, “work”: “工作”, “practice”: “练习”, “improve”: “提高”, “learn”: “学习”, “study”: “学习” } },
{ name: “第三单元”, words: { “nature”: “自然”, “environment”: “环境”, “animal”: “动物”, “plant”: “植物”, “protect”: “保护”, “wild”: “野生的”, “endangered”: “濒危的”, “rare”: “稀有的”, “beautiful”: “美丽的”, “important”: “重要的”, “necessary”: “必要的”, “possible”: “可能的” } }
]
},
12: {
name: “牛津英语高三”,
units: [
{ name: “第一单元”, words: { “examination”: “考试”, “exam”: “考试”, “test”: “测试”, “prepare”: “准备”, “review”: “复习”, “practice”: “练习”, “important”: “重要的”, “difficult”: “困难的”, “challenge”: “挑战”, “success”: “成功”, “result”: “结果”, “score”: “分数” } },
{ name: “第二单元”, words: { “future”: “未来”, “career”: “职业”, “job”: “工作”, “work”: “工作”, “profession”: “专业”, “university”: “大学”, “college”: “学院”, “study”: “学习”, “major”: “专业”, “degree”: “学位”, “opportunity”: “机会”, “choose”: “选择” } },
{ name: “第三单元”, words: { “society”: “社会”, “community”: “社区”, “citizen”: “公民”, “responsibility”: “责任”, “duty”: “义务”, “volunteer”: “志愿者”, “service”: “服务”, “contribute”: “贡献”, “develop”: “发展”, “improve”: “改善”, “change”: “改变”, “help”: “帮助” } }
]
}
},
college: {
1: {
name: “大学英语一年级”,
units: [
{ name: “第一单元”, words: { “curriculum”: “课程”, “semester”: “学期”, “lecture”: “讲座”, “assignment”: “作业”, “scholarship”: “奖学金”, “campus”: “校园”, “dormitory”: “宿舍”, “cafeteria”: “餐厅”, “laboratory”: “实验室”, “library”: “图书馆”, “professor”: “教授”, “lecture”: “讲师”, “tutor”: “导师”, “classmate”: “同学”, “freshman”: “大一新生” } },
{ name: “第二单元”, words: { “syllabus”: “教学大纲”, “textbook”: “教科书”, “notebook”: “笔记本”, “assignment”: “任务”, “deadline”: “截止日期”, “exam”: “考试”, “midterm”: “期中考试”, “final”: “期末考试”, “grade”: “成绩”, “GPA”: “平均绩点”, “credit”: “学分”, “course”: “课程”, “major”: “主修”, “minor”: “辅修” } },
{ name: “第三单元”, words: { “research”: “研究”, “project”: “项目”, “presentation”: “演讲”, “discussion”: “讨论”, “debate”: “辩论”, “essay”: “文章”, “report”: “报告”, “thesis”: “论文”, “bibliography”: “参考文献”, “citation”: “引用”, “plagiarism”: “抄袭”, “academic”: “学术的”, “scholarly”: “学者型的” } }
]
},
2: {
name: “大学英语二年级”,
units: [
{ name: “第一单元”, words: { “internship”: “实习”, “part-time”: “兼职”, “volunteer”: “志愿者”, “extracurricular”: “课外的”, “activity”: “活动”, “club”: “社团”, “organization”: “组织”, “leadership”: “领导力”, “teamwork”: “团队合作”, “communication”: “交流”, “skill”: “技能”, “experience”: “经验”, “resume”: “简历” } },
{ name: “第二单元”, words: { “networking”: “人脉”, “interview”: “面试”, “career”: “职业”, “employer”: “雇主”, “employee”: “员工”, “salary”: “工资”, “benefit”: “福利”, “promotion”: “晋升”, “management”: “管理”, “business”: “商业”, “marketing”: “市场营销”, “finance”: “金融”, “economy”: “经济” } },
{ name: “第三单元”, words: { “technology”: “技术”, “innovation”: “创新”, “engineering”: “工程”, “software”: “软件”, “hardware”: “硬件”, “artificial”: “人工智能”, “robotics”: “机器人技术”, “automation”: “自动化”, “digital”: “数字的”, “virtual”: “虚拟的”, “online”: “在线”, “offline”: “离线的” } }
]
},
3: {
name: “大学英语三年级”,
units: [
{ name: “第一单元”, words: { “specialization”: “专业化”, “advanced”: “高级的”, “intermediate”: “中级的”, “professional”: “专业的”, “vocational”: “职业的”, “practical”: “实用的”, “theoretical”: “理论的”, “comprehensive”: “全面的”, “intensive”: “强化的”, “extensive”: “广泛的”, “selective”: “选择性的”, “optional”: “选修的” } },
{ name: “第二单元”, words: { “journalism”: “新闻学”, “broadcasting”: “广播”, “publication”: “出版”, “editorial”: “编辑的”, “photography”: “摄影”, “graphic”: “平面设计”, “animation”: “动画”, “multimedia”: “多媒体”, “digital”: “数字的”, “broadcast”: “广播”, “media”: “媒体”, “press”: ” press” } },
{ name: “第三单元”, words: { “international”: “国际的”, “global”: “全球的”, “cultural”: “文化的”, “diversity”: “多样性”, “exchange”: “交流”, “abroad”: “在国外”, “overseas”: “海外”, “immigration”: “移民”, “emigration”: “移居”, “citizenship”: “公民身份”, “passport”: “护照”, “visa”: “签证” } }
]
},
4: {
name: “大学英语四年级”,
units: [
{ name: “第一单元”, words: { “graduation”: “毕业”, “ceremony”: “典礼”, “diploma”: “毕业证书”, “degree”: “学位”, “bachelor”: “学士”, “master”: “硕士”, “doctorate”: “博士”, “alumni”: “校友”, “career”: “职业生涯”, “employment”: “就业”, “unemployment”: “失业”, “orientation”: “迎新” } },
{ name: “第二单元”, words: { “entrepreneur”: “企业家”, “startup”: “初创公司”, “venture”: “风险”, “investment”: “投资”, “capital”: “资本”, “funding”: “资金”, “profit”: “利润”, “revenue”: “收入”, “expense”: “支出”, “budget”: “预算”, “tax”: “税”, “audit”: “审计” } },
{ name: “第三单元”, words: { “philosophy”: “哲学”, “psychology”: “心理学”, “sociology”: “社会学”, “anthropology”: “人类学”, “archaeology”: “考古学”, “history”: “历史”, “geography”: “地理”, “philosophical”: “哲学的”, “theoretical”: “理论的”, “empirical”: “经验主义的”, “qualitative”: “定性的”, “quantitative”: “定量的” } }
]
}
},
graduate: {
1: {
name: “研究生英语一年级”,
units: [
{ name: “第一单元”, words: { “methodology”: “方法论”, “hypothesis”: “假设”, “qualitative”: “定性的”, “quantitative”: “定量的”, “empirical”: “经验主义的”, “theoretical”: “理论框架”, “framework”: “框架”, “paradigm”: “范式”, “paradigm shift”: “范式转变”, “paradigm”: “典范”, “model”: “模型” } },
{ name: “第二单元”, words: { “manuscript”: “手稿”, “dissertation”: “学位论文”, “thesis”: “论文”, “journal”: “期刊”, “publication”: “发表”, “peer review”: “同行评审”, “conference”: “会议”, “symposium”: “研讨会”, “symposium”: “专题讨论会”, “workshop”: “工作坊”, “seminar”: “研讨会” } },
{ name: “第三单元”, words: { “research”: “研究”, “investigation”: “调查”, “inquiry”: “询问”, “exploration”: “探索”, “experiment”: “实验”, “observation”: “观察”, “analysis”: “分析”, “synthesis”: “综合”, “evaluation”: “评估”, “assessment”: “评定”, “review”: “综述” } }
]
},
2: {
name: “研究生英语二年级”,
units: [
{ name: “第一单元”, words: { “grant”: “资助”, “fellowship”: “奖学金”, “funding”: “资金”, “sponsorship”: “赞助”, “scholarship”: “奖学金”, “bursary”: “助学金”, “loan”: “贷款”, “budget”: “预算”, “fund”: “基金”, “allocation”: “分配”, “fundraising”: “筹款” } },
{ name: “第二单元”, words: { “academic”: “学术的”, “scholarly”: “学术性的”, “intellectual”: “智力的”, “cognitive”: “认知的”, “critical thinking”: “批判性思维”, “analytical”: “分析的”, “systematic”: “系统的”, “methodical”: “有条理的”, “rigorous”: “严格的”, “thorough”: “彻底的” } },
{ name: “第三单元”, words: { “statistics”: “统计学”, “regression”: “回归”, “correlation”: “相关性”, “variance”: “方差”, “deviation”: “偏差”, “sample”: “样本”, “population”: “总体”, “hypothesis test”: “假设检验”, “confidence interval”: “置信区间”, “significance”: “显著性”, “p-value”: “p值” } }
]
},
3: {
name: “研究生英语三年级”,
units: [
{ name: “第一单元”, words: { “dissertation”: “学位论文”, “defense”: “答辩”, “viva”: “口头答辩”, “examination”: “审查”, “assessment”: “评估”, “evaluation”: “评价”, “panel”: “评审团”, “committee”: “委员会”, “supervisor”: “导师”, “advisor”: “顾问”, “external examiner”: “校外考官” } },
{ name: “第二单元”, words: { “collaboration”: “合作”, “cooperation”: “协作”, “partnership”: “伙伴关系”, “affiliation”: “隶属”, “affiliation”: “机构”, “consortium”: ” consortium”, “network”: “网络”, “consortium”: “联合体”, “alliance”: “联盟”, “joint research”: “联合研究” } },
{ name: “第三单元”, words: { “innovation”: “创新”, “invention”: “发明”, “discovery”: “发现”, “breakthrough”: “突破”, “advancement”: “进步”, “progress”: “进展”, “achievement”: “成就”, “contribution”: “贡献”, “impact”: “影响”, “implication”: “含义”, “significance”: “意义” } }
]
}
}
};

// ==================== GAME STATE ====================
let gameState = {
currentGrade: null,
currentGradeNum: null,
selectedBook: null,
difficulty: ‘medium’,
mode: ‘learn’,
grid: [],
tiles: [],
selectedTile: null,
score: 0,
timeLeft: 0,
timer: null,
matchedPairs: 0,
totalPairs: 0,
hintsLeft: 3,
isPaused: false,
isPlaying: false,
currentUnitIndex: 0
};

// ==================== DIFFICULTY SETTINGS ====================
const difficultySettings = {
easy: { rows: 4, cols: 4, time: 120 },
medium: { rows: 6, cols: 6, time: 180 },
hard: { rows: 8, cols: 8, time: 240 }
};

// ==================== PAGE NAVIGATION ====================
function showPage(pageName) {
document.querySelectorAll(‘.page’).forEach(page => {
page.classList.remove(‘active’);
});
document.getElementById(pageName + ‘-page’).classList.add(‘active’);
}

function showHowToPlay() {
document.getElementById(‘howto-modal’).classList.add(‘active’);
}

function closeHowToPlay() {
document.getElementById(‘howto-modal’).classList.remove(‘active’);
}

// ==================== SELECTION FUNCTIONS ====================
function selectGrade(level, gradeNum) {
gameState.currentGrade = level;
gameState.currentGradeNum = gradeNum;

// Populate book grid
const bookGrid = document.getElementById(‘book-grid’);
const data = vocabularyData[level][gradeNum];

document.getElementById(‘book-section-title’).textContent = data.name + ‘ – 选择单元’;

bookGrid.innerHTML = data.units.map((unit, index) => `
<div class=”book-card” onclick=”selectUnit(${index})” data-unit=”${index}”>
<div class=”book-card-title”>${unit.name}</div>
</div>
`).join(”);

showPage(‘book’);
}

function selectUnit(index) {
gameState.currentUnitIndex = index;

// Update UI
document.querySelectorAll(‘.book-card’).forEach((card, i) => {
card.classList.toggle(‘selected’, i === index);
});

gameState.selectedBook = vocabularyData[gameState.currentGrade][gameState.currentGradeNum].units[index];
}

function selectDifficulty(difficulty) {
gameState.difficulty = difficulty;
document.querySelectorAll(‘.difficulty-btn’).forEach(btn => {
btn.classList.toggle(‘active’, btn.dataset.difficulty === difficulty);
});
}

function selectMode(mode) {
gameState.mode = mode;
document.querySelectorAll(‘.mode-btn’).forEach(btn => {
btn.classList.toggle(‘active’, btn.dataset.mode === mode);
});
}

// ==================== GAME INITIALIZATION ====================
function startGame() {
if (!gameState.selectedBook) {
alert(‘请选择一个单元!’);
return;
}

// Reset game state
gameState.score = 0;
gameState.matchedPairs = 0;
gameState.hintsLeft = 3;
gameState.isPaused = false;
gameState.isPlaying = true;
gameState.selectedTile = null;

const settings = difficultySettings[gameState.difficulty];
gameState.timeLeft = settings.time;

// Get words for the game
const words = gameState.selectedBook.words;
const wordEntries = Object.entries(words);

// Calculate how many pairs we can fit
const maxPairs = Math.floor((settings.rows * settings.cols) / 2);
const numPairs = Math.min(wordEntries.length, maxPairs);

// Shuffle and select words
const shuffledWords = wordEntries.sort(() => Math.random() – 0.5).slice(0, numPairs);

// Create tile data
let tiles = [];
shuffledWords.forEach(([english, chinese]) => {
tiles.push({ id: english, text: english, type: ‘english’, pair: english });
tiles.push({ id: chinese, text: chinese, type: ‘chinese’, pair: english });
});

// Shuffle tiles
tiles.sort(() => Math.random() – 0.5);

// Fill remaining space if needed
while (tiles.length < settings.rows * settings.cols) {
tiles.push({ id: ’empty’, text: ”, type: ’empty’, pair: null });
}

gameState.tiles = tiles;
gameState.totalPairs = numPairs;

// Update UI
document.getElementById(‘current-book’).textContent =
`${vocabularyData[gameState.currentGrade][gameState.currentGradeNum].name} – ${gameState.selectedBook.name}`;
document.getElementById(‘score’).textContent = ‘0’;
document.getElementById(‘matched’).textContent = `0 / ${numPairs}`;
document.getElementById(‘hints-left’).textContent = ‘3’;
document.getElementById(‘timer’).textContent = formatTime(gameState.timeLeft);

// Generate game grid
generateGrid(settings.rows, settings.cols);

// Generate word list sidebar
generateWordList(shuffledWords);

// Show game page
showPage(‘game’);

// Start timer
startTimer();

// Save progress
saveProgress();
}

function generateGrid(rows, cols) {
const grid = document.getElementById(‘game-grid’);
grid.style.gridTemplateColumns = `repeat(${cols}, 80px)`;
grid.innerHTML = ”;

// Clear SVG
document.getElementById(‘connection-svg’).innerHTML = ”;

gameState.tiles.forEach((tile, index) => {
const tileEl = document.createElement(‘div’);
tileEl.className = `game-tile ${tile.type}`;
tileEl.dataset.index = index;
tileEl.textContent = tile.text;

if (tile.type === ’empty’) {
tileEl.classList.add(’empty’);
}

tileEl.addEventListener(‘click’, () => handleTileClick(index));
grid.appendChild(tileEl);
});
}

function generateWordList(words) {
const wordList = document.getElementById(‘word-list’);
wordList.innerHTML = words.map(([english, chinese]) => `
<div class=”word-item” data-pair=”${english}”>
<span class=”word-english”>${english}</span>
<span class=”word-chinese”>${chinese}</span>
</div>
`).join(”);
}

// ==================== GAME LOGIC ====================
function handleTileClick(index) {
if (gameState.isPaused || !gameState.isPlaying) return;

const tiles = document.querySelectorAll(‘.game-tile’);
const clickedTile = gameState.tiles[index];

// Ignore empty tiles or already matched
if (clickedTile.type === ’empty’ || clickedTile.matched) return;

// If same tile clicked, deselect
if (gameState.selectedTile === index) {
tiles[index].classList.remove(‘selected’);
gameState.selectedTile = null;
return;
}

// If no tile selected, select this one
if (gameState.selectedTile === null) {
gameState.selectedTile = index;
tiles[index].classList.add(‘selected’);
playSound(‘click’);
return;
}

// Check if clicked tile is a match
const firstTile = gameState.tiles[gameState.selectedTile];
const secondTile = clickedTile;

// Must be different types (one English, one Chinese)
if (firstTile.type === secondTile.type) {
// Different types – show error
tiles[gameState.selectedTile].classList.add(‘error’);
tiles[index].classList.add(‘error’);
playSound(‘error’);

setTimeout(() => {
tiles[gameState.selectedTile].classList.remove(‘selected’, ‘error’);
tiles[index].classList.remove(‘error’);
gameState.selectedTile = null;
}, 400);
return;
}

// Check if they are a matching pair – simplified logic, no path finding needed
if (firstTile.pair === secondTile.pair) {
// Match found! Simple direct match
playSound(‘match’);
matchTiles(gameState.selectedTile, index);
} else {
// Not a matching pair – show error
tiles[gameState.selectedTile].classList.add(‘error’);
tiles[index].classList.add(‘error’);
playSound(‘error’);

setTimeout(() => {
tiles[gameState.selectedTile].classList.remove(‘selected’, ‘error’);
tiles[index].classList.remove(‘error’);
gameState.selectedTile = null;
}, 400);
}
}

// Simplified match function – no path finding needed
function matchTiles(index1, index2) {
const tiles = document.querySelectorAll(‘.game-tile’);

// Update game state
gameState.tiles[index1].matched = true;
gameState.tiles[index2].matched = true;

// Update UI
tiles[index1].classList.remove(‘selected’);
tiles[index1].classList.add(‘matched’);
tiles[index2].classList.add(‘matched’);

// Update score
gameState.score += 10;
gameState.matchedPairs++;
document.getElementById(‘score’).textContent = gameState.score;
document.getElementById(‘matched’).textContent =
`${gameState.matchedPairs} / ${gameState.totalPairs}`;

// Update progress
const progress = (gameState.matchedPairs / gameState.totalPairs) * 100;
document.getElementById(‘progress-fill’).style.width = `${progress}%`;
document.getElementById(‘progress-text’).textContent = `${Math.round(progress)}% 完成`;

// Update word list
const pair = gameState.tiles[index1].pair;
document.querySelectorAll(`.word-item[data-pair=”${pair}”]`).forEach(item => {
item.classList.add(‘matched’);
});

// Reset selection
gameState.selectedTile = null;

// Create particle effect
createParticles(tiles[index1]);
createParticles(tiles[index2]);

// Check win condition
if (gameState.matchedPairs >= gameState.totalPairs) {
setTimeout(showVictory, 500);
}
}

function createParticles(tile) {
const rect = tile.getBoundingClientRect();
const colors = [‘#4CAF50’, ‘#81C784’, ‘#A5D6A7’, ‘#FF9800’];

for (let i = 0; i < 8; i++) {
const particle = document.createElement(‘div’);
particle.className = ‘particle’;
particle.style.background = colors[Math.floor(Math.random() * colors.length)];
particle.style.left = `${rect.left + rect.width / 2}px`;
particle.style.top = `${rect.top + rect.height / 2}px`;

const angle = (Math.PI * 2 * i) / 8;
const distance = 30 + Math.random() * 20;
particle.style.setProperty(‘–tx’, `${Math.cos(angle) * distance}px`);
particle.style.setProperty(‘–ty’, `${Math.sin(angle) * distance}px`);

document.body.appendChild(particle);

setTimeout(() => particle.remove(), 800);
}
}

// ==================== GAME CONTROLS ====================
function useHint() {
if (gameState.hintsLeft <= 0 || gameState.isPaused) return;

// Find a random unmatched pair
const unmatched = [];
const seenPairs = new Set();

gameState.tiles.forEach((tile, index) => {
if (!tile.matched && tile.pair && !seenPairs.has(tile.pair)) {
seenPairs.add(tile.pair);
unmatched.push(tile.pair);
}
});

if (unmatched.length === 0) return;

const hintPair = unmatched[Math.floor(Math.random() * unmatched.length)];
const tiles = document.querySelectorAll(‘.game-tile’);

gameState.tiles.forEach((tile, index) => {
if (tile.pair === hintPair) {
tiles[index].classList.add(‘hint’);
setTimeout(() => {
tiles[index].classList.remove(‘hint’);
}, 2000);
}
});

gameState.hintsLeft–;
document.getElementById(‘hints-left’).textContent = gameState.hintsLeft;
}

function shuffleTiles() {
// Get all unmatched tiles
const unmatched = gameState.tiles
.map((tile, index) => ({ tile, index }))
.filter(({ tile }) => !tile.matched && tile.type !== ’empty’);

// Extract the tiles
const tiles = unmatched.map(({ tile }) => tile);

// Shuffle
tiles.sort(() => Math.random() – 0.5);

// Put back
unmatched.forEach(({ index }, i) => {
gameState.tiles[index] = tiles[i];
});

// Re-render grid
const settings = difficultySettings[gameState.difficulty];
generateGrid(settings.rows, settings.cols);
}

function pauseGame() {
if (!gameState.isPlaying) return;
gameState.isPaused = true;
clearInterval(gameState.timer);
document.getElementById(‘pause-modal’).classList.add(‘active’);
}

function resumeGame() {
gameState.isPaused = false;
document.getElementById(‘pause-modal’).classList.remove(‘active’);
startTimer();
}

function quitGame() {
clearInterval(gameState.timer);
gameState.isPlaying = false;
showPage(‘home’);
}

// ==================== TIMER ====================
function startTimer() {
clearInterval(gameState.timer);
gameState.timer = setInterval(() => {
if (gameState.isPaused) return;

gameState.timeLeft–;

const timerEl = document.getElementById(‘timer’);
timerEl.textContent = formatTime(gameState.timeLeft);

if (gameState.timeLeft <= 30) {
timerEl.classList.add(‘timer-warning’);
} else {
timerEl.classList.remove(‘timer-warning’);
}

if (gameState.timeLeft <= 0) {
clearInterval(gameState.timer);
showGameOver();
}
}, 1000);
}

function formatTime(seconds) {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins.toString().padStart(2, ‘0’)}:${secs.toString().padStart(2, ‘0’)}`;
}

// ==================== MODALS ====================
function showVictory() {
clearInterval(gameState.timer);
gameState.isPlaying = false;
playSound(‘win’);

// Calculate stars
const settings = difficultySettings[gameState.difficulty];
const timeUsed = settings.time – gameState.timeLeft;
let stars = 1;
if (timeUsed < settings.time * 0.5) stars = 3;
else if (timeUsed < settings.time * 0.75) stars = 2;

// Update stars display
const starsEl = document.getElementById(‘stars’);
starsEl.innerHTML = ”;
for (let i = 0; i < 3; i++) {
const star = document.createElement(‘span’);
star.className = `star ${i < stars ? ‘filled’ : ”}`;
star.textContent = ‘★’;
starsEl.appendChild(star);
}

document.getElementById(‘final-score’).textContent = gameState.score;
document.getElementById(‘final-time’).textContent = formatTime(settings.time – gameState.timeLeft);
document.getElementById(‘victory-modal’).classList.add(‘active’);

// Save high score
saveProgress();
}

function showGameOver() {
gameState.isPlaying = false;
playSound(‘gameover’);
document.getElementById(‘gameover-score’).textContent = gameState.score;
document.getElementById(‘gameover-matched’).textContent = gameState.matchedPairs;
document.getElementById(‘gameover-modal’).classList.add(‘active’);
}

function restartGame() {
document.getElementById(‘victory-modal’).classList.remove(‘active’);
document.getElementById(‘gameover-modal’).classList.remove(‘active’);
startGame();
}

function nextLevel() {
document.getElementById(‘victory-modal’).classList.remove(‘active’);

const data = vocabularyData[gameState.currentGrade][gameState.currentGradeNum];
const nextUnitIndex = gameState.currentUnitIndex + 1;

if (nextUnitIndex < data.units.length) {
selectUnit(nextUnitIndex);
startGame();
} else {
alert(‘恭喜!你已完成本年级的所有单元!’);
showPage(‘home’);
}
}

// ==================== SOUND SYSTEM ====================
let soundEnabled = true;

// Initialize sound settings from localStorage
function initSound() {
const savedSound = localStorage.getItem(‘oxfordGameSound’);
if (savedSound !== null) {
soundEnabled = savedSound === ‘true’;
}
updateSoundIcon();
}

// Toggle sound on/off
function toggleSound() {
soundEnabled = !soundEnabled;
localStorage.setItem(‘oxfordGameSound’, soundEnabled.toString());
updateSoundIcon();
if (soundEnabled) {
playSound(‘click’);
}
}

// Update the sound icon based on current state
function updateSoundIcon() {
const soundIcon = document.getElementById(‘sound-icon’);
const soundToggle = document.getElementById(‘sound-toggle’);
if (soundEnabled) {
soundIcon.textContent = ‘🔊’;
soundToggle.classList.remove(‘muted’);
} else {
soundIcon.textContent = ‘🔇’;
soundToggle.classList.add(‘muted’);
}
}

// Play sound effect (using Web Audio API for generated sounds)
function playSound(type) {
if (!soundEnabled) return;

try {
const audioContext = new (window.AudioContext || window.webkitAudioContext)();
const oscillator = audioContext.createOscillator();
const gainNode = audioContext.createGain();

oscillator.connect(gainNode);
gainNode.connect(audioContext.destination);

switch(type) {
case ‘match’:
// Happy chime sound for correct match
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime); // C5
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.1); // E5
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.2); // G5
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.4);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.4);
break;
case ‘error’:
// Low buzz for error
oscillator.frequency.setValueAtTime(200, audioContext.currentTime);
oscillator.type = ‘sawtooth’;
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.3);
break;
case ‘click’:
// Soft click for selection
oscillator.frequency.setValueAtTime(800, audioContext.currentTime);
oscillator.type = ‘sine’;
gainNode.gain.setValueAtTime(0.1, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.1);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.1);
break;
case ‘win’:
// Victory fanfare
oscillator.frequency.setValueAtTime(523.25, audioContext.currentTime);
oscillator.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.15);
oscillator.frequency.setValueAtTime(783.99, audioContext.currentTime + 0.3);
oscillator.frequency.setValueAtTime(1046.50, audioContext.currentTime + 0.45);
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.6);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.6);
break;
case ‘gameover’:
// Sad sound for game over
oscillator.frequency.setValueAtTime(400, audioContext.currentTime);
oscillator.frequency.setValueAtTime(350, audioContext.currentTime + 0.2);
oscillator.frequency.setValueAtTime(300, audioContext.currentTime + 0.4);
oscillator.type = ‘sine’;
gainNode.gain.setValueAtTime(0.3, audioContext.currentTime);
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.6);
oscillator.start(audioContext.currentTime);
oscillator.stop(audioContext.currentTime + 0.6);
break;
}
} catch (e) {
console.log(‘Audio playback failed:’, e);
}
}

// ==================== LOCAL STORAGE ====================
function saveProgress() {
const progress = {
lastGrade: gameState.currentGrade,
lastGradeNum: gameState.currentGradeNum,
lastUnit: gameState.currentUnitIndex,
highScore: Math.max(gameState.score, parseInt(localStorage.getItem(‘oxfordGameHighScore’) || ‘0’))
};
localStorage.setItem(‘oxfordGameProgress’, JSON.stringify(progress));
localStorage.setItem(‘oxfordGameHighScore’, progress.highScore.toString());
}

function loadProgress() {
const saved = localStorage.getItem(‘oxfordGameProgress’);
if (saved) {
// Could restore progress here if needed
}
}

// ==================== INITIALIZATION ====================
document.addEventListener(‘DOMContentLoaded’, () => {
loadProgress();
initSound();
});
</script>

<style>

* {
margin: 0;
padding: 0;
box-sizing: border-box;
}

body {
font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;
}

.inland-security-footer {
position: fixed;
z-index: 10;
bottom: 0;
right: 0;
width: fit-content;
display: flex;
justify-content: flex-end;
}

.tooltip-container {
position: relative;
}

.trigger-content {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: center;
padding: 8px;
background-color: transparent;
border-radius: 8px;
cursor: pointer;
}

.minimax-link {
display: flex;
align-items: center;
cursor: pointer;
font-weight: 300;
color: #adadad;
text-decoration: none;
}

.minimax-link:visited {
color: #adadad;
}

.minimax-link:hover {
color: #666666;
}

.tooltip-content {
position: absolute;
bottom: 100%;
right: 0;
display: none;
flex-direction: column;
align-items: center;
gap: 8px;
padding: 8px;
padding-bottom: 16px; /* 8px 内容间距 + 8px 用于鼠标移动的透明区域 */
font-size: 12px;
line-height: 17px;
background-color: #fafafa;
border-radius: 12px;
white-space: nowrap;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
color: #171717;
}

/* 创建透明的连接区域,让鼠标可以平滑移动到 tooltip */
.tooltip-content::after {
content: ”;
position: absolute;
bottom: -8px;
left: 0;
right: 0;
height: 16px;
}

.tooltip-container:hover .tooltip-content {
display: flex;
}

.info-section {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 12px;
}

.beian-link {
display: flex;
align-items: center;
text-decoration: none;
color: #171717;
}

.beian-icon {
width: 14px;
height: 14px;
margin-right: 2px;
}

.underline-text {
color: #171717;
text-decoration: underline;
}

.underline-text:hover {
color: #000000;
}

/* 深色模式支持 */
@media (prefers-color-scheme: dark) {
.trigger-content {
background-color: transparent;
}

.minimax-link,
.minimax-link:visited {
color: #666666;
}

.minimax-link:hover {
color: #adadad;
}

.tooltip-content {
background-color: #262626;
color: #ededed;
}

.beian-link,
.underline-text {
color: #ededed;
}

.underline-text:hover {
color: #ffffff;
}
}

</style>
<div class=”inland-security-footer”>
<div class=”tooltip-container”>
<div class=”trigger-content”>
<a href=”https://www.minimaxi.com/” target=”_blank” rel=”noreferrer” class=”minimax-link”>
<span>© 2026 MiniMax</span>
</a>
</div>
<div class=”tooltip-content”>
<section class=”info-section”>
<span>上海稀宇科技有限公司</span>
</section>

<section class=”info-section”>
<a class=”beian-link” href=”https://www.beian.gov.cn/portal/registerSystemInfo?recordcode=31010402010179″ target=”_blank” rel=”noreferrer”>
<img alt=”hailuo-beian-icon” src=”https://cdn.hailuoai.com/hailuo-video-web/public_assets/223037d3-79fd-4db0-a456-749f38a6a14b.png” class=”beian-icon”>
<span class=”underline-text”>沪公网安备31010402010179号</span>
</a>
<a href=”https://beian.miit.gov.cn/” target=”_blank” rel=”noreferrer” class=”underline-text”>
沪ICP备2023003282号-38
</a>
</section>

<section class=”info-section”>
<span>模型名称:MiniMax</span>
<span>备案号:Shanghai-MiniMax-202505230046</span>
</section>

<section class=”info-section”>
<a href=”https://agent.minimaxi.com/doc/zh/terms-of-service.html” target=”_blank” rel=”noreferrer” class=”underline-text”>
用户协议
</a>
<a href=”https://agent.minimaxi.com/doc/zh/privacy-policy.html” target=”_blank” rel=”noreferrer” class=”underline-text”>
隐私政策
</a>
</section>
</div>
</div>
</div>

<script>
/**
* Iframe 元素高亮注入脚本
* 需要在目标网站中引入此脚本来支持跨域 iframe 高亮功能
*
* 使用方法:
* 1. 将此脚本添加到目标网站的 HTML 中
* 2. 或通过浏览器扩展、用户脚本等方式注入
*/

(function () {
“use strict”;

// 检查是否在 iframe 中
if (window.self === window.top) {
return; // 不在 iframe 中,不执行
}

// 检查是否已经初始化过
if (window.__iframeHighlightInitialized) {
return;
}
window.__iframeHighlightInitialized = true;
console.log(“Iframe 高亮脚本已加载”);

// 创建高亮覆盖层
var overlay = document.createElement(“div”);
overlay.id = “iframe-highlight-overlay”;
overlay.style.cssText = “\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 999999;\n overflow: hidden;\n “;

// 创建悬停高亮框(虚线边框)
var highlightBox = document.createElement(“div”);
highlightBox.id = “iframe-highlight-box”;
highlightBox.style.cssText = “\n position: absolute;\n border: 2px dashed #007AFF;\n background: rgba(0, 122, 255, 0.08);\n pointer-events: none;\n display: none;\n transition: all 0.1s ease;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);\n border-radius: 2px;\n “;

// 创建选中节点的常驻高亮框(实线边框)
var selectedBox = document.createElement(“div”);
selectedBox.id = “iframe-selected-box”;
selectedBox.style.cssText = “\n position: absolute;\n border: 2px solid #007AFF;\n pointer-events: none;\n display: none;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 107, 53, 0.4);\n border-radius: 2px;\n z-index: 1000000;\n “;

// 创建悬停标签显示
var tagLabel = document.createElement(“div”);
tagLabel.id = “iframe-tag-label”;
tagLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 2px 6px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 2px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000001;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n font-weight: 500;\n “;

// 创建选中节点标签
var selectedLabel = document.createElement(“div”);
selectedLabel.id = “iframe-selected-label”;
selectedLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 3px 8px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 3px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000002;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);\n font-weight: 600;\n “;
overlay.appendChild(highlightBox);
overlay.appendChild(selectedBox);
overlay.appendChild(tagLabel);
overlay.appendChild(selectedLabel);
document.body.appendChild(overlay);

// 存储当前选中的元素
var selectedElement = null;
var highlightEnabled = false;

// 更新选中元素的高亮显示
function updateSelectedHighlight(element) {
console.log(“updateSelectedHighlight called with:”, element);
if (!element) {
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
selectedElement = null;
console.log(“Cleared selected highlight”);
return;
}
selectedElement = element;
var rect = element.getBoundingClientRect();
console.log(“Selected element rect:”, rect);

// 更新选中高亮框位置
selectedBox.style.display = “block”;
selectedBox.style.left = “”.concat(rect.left – 2, “px”);
selectedBox.style.top = “”.concat(rect.top – 2, “px”);
selectedBox.style.width = “”.concat(rect.width + 4, “px”);
selectedBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新选中标签位置和内容
selectedLabel.style.display = “block”;
selectedLabel.textContent = “\u2713 <“.concat(element.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 28;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 5) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
var labelWidth = selectedLabel.offsetWidth || 100; // 预估宽度
if (labelLeft + labelWidth > window.innerWidth – 10) {
labelLeft = window.innerWidth – labelWidth – 10;
}
selectedLabel.style.left = “”.concat(Math.max(5, labelLeft), “px”);
selectedLabel.style.top = “”.concat(labelTop, “px”);
console.log(“Selected highlight positioned at:”, {
left: selectedBox.style.left,
top: selectedBox.style.top,
width: selectedBox.style.width,
height: selectedBox.style.height
});
}
function getElementSelector(element) {
if (!(element instanceof Element)) throw new Error(‘Argument must be a DOM element’);
var segments = [];
var current = element;
while (current !== document.documentElement) {
var selector = ”;
// 优先检查唯一ID
if (current.id && document.querySelectorAll(“#”.concat(current.id)).length === 1) {
segments.unshift(“#”.concat(current.id));
break; // ID唯一,无需继续向上
}

// 生成类名选择器(取第一个有效类名)
var classes = Array.from(current.classList).filter(function (c) {
return !c.startsWith(‘js-‘);
});
var className = classes.length > 0 ? “.”.concat(classes[0]) : ”;

// 生成位置索引(nth-child)
var tag = current.tagName.toLowerCase();
if (!className) {
var siblings = Array.from(current.parentNode.children);
var index = siblings.findIndex(function (el) {
return el === current;
}) + 1;
selector = “”.concat(tag, “:nth-child(“).concat(index, “)”);
} else {
selector = className;
}
segments.unshift(selector);
current = current.parentElement;
}

// 处理根元素
if (current === document.documentElement) {
segments.unshift(‘html’);
}
return segments.join(‘ > ‘);
}

// 获取元素文本内容
function getElementText(element) {
var _element$textContent;
if (element.tagName === “INPUT”) {
return element.value || element.placeholder || “”;
}
if (element.tagName === “TEXTAREA”) {
return element.value || element.placeholder || “”;
}
var text = ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) || “”;
return text.length > 50 ? text.substring(0, 50) + “…” : text;
}

// 获取元素属性信息
function getElementAttributes(element) {
var attrs = {};
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
return attrs;
}

// 鼠标悬停事件处理
function handleMouseOver(e) {
if (!highlightEnabled) return;
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免高亮 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 如果是已选中的元素,不显示悬停高亮
if (target === selectedElement) {
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
return;
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);

// 更新悬停高亮框位置
highlightBox.style.display = “block”;
highlightBox.style.left = “”.concat(rect.left – 2, “px”);
highlightBox.style.top = “”.concat(rect.top – 2, “px”);
highlightBox.style.width = “”.concat(rect.width + 4, “px”);
highlightBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新标签位置和内容
tagLabel.style.display = “block”;
tagLabel.textContent = “<“.concat(target.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 22;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 0) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
if (labelLeft + tagLabel.offsetWidth > window.innerWidth) {
labelLeft = window.innerWidth – tagLabel.offsetWidth – 5;
}
tagLabel.style.left = “”.concat(Math.max(0, labelLeft), “px”);
tagLabel.style.top = “”.concat(labelTop, “px”);

// 发送消息到父窗口
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 鼠标离开事件处理
function handleMouseOut(e) {
if (!highlightEnabled) return;
var relatedTarget = e.relatedTarget;

// 如果鼠标移动到高亮相关元素上,不隐藏高亮
if (relatedTarget && (relatedTarget === highlightBox || relatedTarget === tagLabel || relatedTarget === overlay || relatedTarget === selectedBox || relatedTarget === selectedLabel)) {
return;
}
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: null,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 点击事件处理
function handleClick(e) {
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免处理 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 检查是否是交互元素,这些元素需要保留默认行为
var isInteractiveElement = [‘input’, ‘textarea’, ‘select’, ‘button’, ‘a’].includes(target.tagName.toLowerCase());

// 如果高亮功能启用,对于非交互元素阻止默认行为和事件传播
if (highlightEnabled) {
e.preventDefault();
e.stopPropagation();
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
console.log(“Element clicked:”, {
tagName: target.tagName,
selector: selector,
rect: rect
});

// 立即更新选中高亮
updateSelectedHighlight(target);

// 隐藏悬停高亮,因为现在是选中状态
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-click”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 监听来自父窗口的消息
function handleParentMessage(event) {
console.log(“Received message from parent:”, event.data);
if (event.data.type === “iframe-highlight-toggle”) {
var enabled = event.data.enabled;
console.log(“Highlight toggle:”, enabled);
if (enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “enable-iframe-highlight”) {
console.log(“Enable iframe highlight”);
enableHighlight();
} else if (event.data.type === “disable-iframe-highlight”) {
console.log(“Disable iframe highlight”);
disableHighlight();
} else if (event.data.type === “toggle-iframe-highlight”) {
var _enabled = event.data.enabled !== undefined ? event.data.enabled : !highlightEnabled;
console.log(“Toggle iframe highlight to:”, _enabled);
if (_enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “update-selected-element”) {
var selector = event.data.selector;
console.log(“Update selected element with selector:”, selector);
if (selector) {
try {
var element = document.querySelector(selector);
console.log(“Found element by selector:”, element);
updateSelectedHighlight(element);
} catch (error) {
console.warn(“Failed to select element:”, error);
updateSelectedHighlight(null);
}
} else {
updateSelectedHighlight(null);
}
} else if (event.data.type === “clear-selected-element”) {
console.log(“Clear selected element”);
updateSelectedHighlight(null);
}
}

// 启用高亮功能
function enableHighlight() {
console.log(“Enabling highlight”);
document.addEventListener(“mouseover”, handleMouseOver, true);
document.addEventListener(“mouseout”, handleMouseOut, true);
document.addEventListener(“click”, handleClick, true);
highlightEnabled = true;
overlay.style.display = “block”;
}

// 禁用高亮功能
function disableHighlight() {
console.log(“Disabling highlight”);
highlightEnabled = false;
// 保持事件监听器,但通过 highlightEnabled 变量控制行为
// 这样可以保留选中状态的显示
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
// 不隐藏 selectedBox 和 selectedLabel,保留选中状态
}

// 完全禁用高亮功能(移除所有监听器)
function fullyDisableHighlight() {
console.log(“Fully disabling highlight”);
highlightEnabled = false;
document.removeEventListener(“mouseover”, handleMouseOver, true);
document.removeEventListener(“mouseout”, handleMouseOut, true);
document.removeEventListener(“click”, handleClick, true);
overlay.style.display = “none”;
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
}

// 添加事件监听
enableHighlight();
window.addEventListener(“message”, handleParentMessage);

// 暴露全局函数供外部调用
window.__iframeHighlightControl = {
enable: enableHighlight,
disable: disableHighlight,
fullyDisable: fullyDisableHighlight,
isEnabled: function isEnabled() {
return highlightEnabled;
},
getSelectedElement: function getSelectedElement() {
return selectedElement;
},
updateSelected: updateSelectedHighlight,
// 通过消息发送开关控制
sendToggleMessage: function sendToggleMessage(enabled) {
window.parent.postMessage({
type: ‘iframe-highlight-status’,
enabled: enabled || highlightEnabled,
source: ‘iframe-highlight-injector’
}, ‘*’);
}
};

// 通知父窗口脚本已加载
try {
window.parent.postMessage({
type: “iframe-highlight-ready”,
data: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
},
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送就绪消息到父窗口:”, error);
}

// 清理函数
window.__iframeHighlightCleanup = function () {
fullyDisableHighlight();
window.removeEventListener(“message”, handleParentMessage);
if (overlay.parentElement) {
overlay.parentElement.removeChild(overlay);
}
delete window.__iframeHighlightInitialized;
delete window.__iframeHighlightCleanup;
};
})();

</script>

<script>
/**
* Iframe 元素高亮注入脚本
* 需要在目标网站中引入此脚本来支持跨域 iframe 高亮功能
*
* 使用方法:
* 1. 将此脚本添加到目标网站的 HTML 中
* 2. 或通过浏览器扩展、用户脚本等方式注入
*/

(function () {
“use strict”;

// 检查是否在 iframe 中
if (window.self === window.top) {
return; // 不在 iframe 中,不执行
}

// 检查是否已经初始化过
if (window.__iframeHighlightInitialized) {
return;
}
window.__iframeHighlightInitialized = true;
console.log(“Iframe 高亮脚本已加载”);

// 创建高亮覆盖层
var overlay = document.createElement(“div”);
overlay.id = “iframe-highlight-overlay”;
overlay.style.cssText = “\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 999999;\n overflow: hidden;\n “;

// 创建悬停高亮框(虚线边框)
var highlightBox = document.createElement(“div”);
highlightBox.id = “iframe-highlight-box”;
highlightBox.style.cssText = “\n position: absolute;\n border: 2px dashed #007AFF;\n background: rgba(0, 122, 255, 0.08);\n pointer-events: none;\n display: none;\n transition: all 0.1s ease;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);\n border-radius: 2px;\n “;

// 创建选中节点的常驻高亮框(实线边框)
var selectedBox = document.createElement(“div”);
selectedBox.id = “iframe-selected-box”;
selectedBox.style.cssText = “\n position: absolute;\n border: 2px solid #007AFF;\n pointer-events: none;\n display: none;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 107, 53, 0.4);\n border-radius: 2px;\n z-index: 1000000;\n “;

// 创建悬停标签显示
var tagLabel = document.createElement(“div”);
tagLabel.id = “iframe-tag-label”;
tagLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 2px 6px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 2px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000001;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n font-weight: 500;\n “;

// 创建选中节点标签
var selectedLabel = document.createElement(“div”);
selectedLabel.id = “iframe-selected-label”;
selectedLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 3px 8px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 3px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000002;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);\n font-weight: 600;\n “;
overlay.appendChild(highlightBox);
overlay.appendChild(selectedBox);
overlay.appendChild(tagLabel);
overlay.appendChild(selectedLabel);
document.body.appendChild(overlay);

// 存储当前选中的元素
var selectedElement = null;
var highlightEnabled = false;

// 更新选中元素的高亮显示
function updateSelectedHighlight(element) {
console.log(“updateSelectedHighlight called with:”, element);
if (!element) {
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
selectedElement = null;
console.log(“Cleared selected highlight”);
return;
}
selectedElement = element;
var rect = element.getBoundingClientRect();
console.log(“Selected element rect:”, rect);

// 更新选中高亮框位置
selectedBox.style.display = “block”;
selectedBox.style.left = “”.concat(rect.left – 2, “px”);
selectedBox.style.top = “”.concat(rect.top – 2, “px”);
selectedBox.style.width = “”.concat(rect.width + 4, “px”);
selectedBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新选中标签位置和内容
selectedLabel.style.display = “block”;
selectedLabel.textContent = “\u2713 <“.concat(element.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 28;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 5) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
var labelWidth = selectedLabel.offsetWidth || 100; // 预估宽度
if (labelLeft + labelWidth > window.innerWidth – 10) {
labelLeft = window.innerWidth – labelWidth – 10;
}
selectedLabel.style.left = “”.concat(Math.max(5, labelLeft), “px”);
selectedLabel.style.top = “”.concat(labelTop, “px”);
console.log(“Selected highlight positioned at:”, {
left: selectedBox.style.left,
top: selectedBox.style.top,
width: selectedBox.style.width,
height: selectedBox.style.height
});
}
function getElementSelector(element) {
if (!(element instanceof Element)) throw new Error(‘Argument must be a DOM element’);
var segments = [];
var current = element;
while (current !== document.documentElement) {
var selector = ”;
// 优先检查唯一ID
if (current.id && document.querySelectorAll(“#”.concat(current.id)).length === 1) {
segments.unshift(“#”.concat(current.id));
break; // ID唯一,无需继续向上
}

// 生成类名选择器(取第一个有效类名)
var classes = Array.from(current.classList).filter(function (c) {
return !c.startsWith(‘js-‘);
});
var className = classes.length > 0 ? “.”.concat(classes[0]) : ”;

// 生成位置索引(nth-child)
var tag = current.tagName.toLowerCase();
if (!className) {
var siblings = Array.from(current.parentNode.children);
var index = siblings.findIndex(function (el) {
return el === current;
}) + 1;
selector = “”.concat(tag, “:nth-child(“).concat(index, “)”);
} else {
selector = className;
}
segments.unshift(selector);
current = current.parentElement;
}

// 处理根元素
if (current === document.documentElement) {
segments.unshift(‘html’);
}
return segments.join(‘ > ‘);
}

// 获取元素文本内容
function getElementText(element) {
var _element$textContent;
if (element.tagName === “INPUT”) {
return element.value || element.placeholder || “”;
}
if (element.tagName === “TEXTAREA”) {
return element.value || element.placeholder || “”;
}
var text = ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) || “”;
return text.length > 50 ? text.substring(0, 50) + “…” : text;
}

// 获取元素属性信息
function getElementAttributes(element) {
var attrs = {};
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
return attrs;
}

// 鼠标悬停事件处理
function handleMouseOver(e) {
if (!highlightEnabled) return;
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免高亮 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 如果是已选中的元素,不显示悬停高亮
if (target === selectedElement) {
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
return;
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);

// 更新悬停高亮框位置
highlightBox.style.display = “block”;
highlightBox.style.left = “”.concat(rect.left – 2, “px”);
highlightBox.style.top = “”.concat(rect.top – 2, “px”);
highlightBox.style.width = “”.concat(rect.width + 4, “px”);
highlightBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新标签位置和内容
tagLabel.style.display = “block”;
tagLabel.textContent = “<“.concat(target.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 22;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 0) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
if (labelLeft + tagLabel.offsetWidth > window.innerWidth) {
labelLeft = window.innerWidth – tagLabel.offsetWidth – 5;
}
tagLabel.style.left = “”.concat(Math.max(0, labelLeft), “px”);
tagLabel.style.top = “”.concat(labelTop, “px”);

// 发送消息到父窗口
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 鼠标离开事件处理
function handleMouseOut(e) {
if (!highlightEnabled) return;
var relatedTarget = e.relatedTarget;

// 如果鼠标移动到高亮相关元素上,不隐藏高亮
if (relatedTarget && (relatedTarget === highlightBox || relatedTarget === tagLabel || relatedTarget === overlay || relatedTarget === selectedBox || relatedTarget === selectedLabel)) {
return;
}
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: null,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 点击事件处理
function handleClick(e) {
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免处理 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 检查是否是交互元素,这些元素需要保留默认行为
var isInteractiveElement = [‘input’, ‘textarea’, ‘select’, ‘button’, ‘a’].includes(target.tagName.toLowerCase());

// 如果高亮功能启用,对于非交互元素阻止默认行为和事件传播
if (highlightEnabled) {
e.preventDefault();
e.stopPropagation();
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
console.log(“Element clicked:”, {
tagName: target.tagName,
selector: selector,
rect: rect
});

// 立即更新选中高亮
updateSelectedHighlight(target);

// 隐藏悬停高亮,因为现在是选中状态
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-click”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 监听来自父窗口的消息
function handleParentMessage(event) {
console.log(“Received message from parent:”, event.data);
if (event.data.type === “iframe-highlight-toggle”) {
var enabled = event.data.enabled;
console.log(“Highlight toggle:”, enabled);
if (enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “enable-iframe-highlight”) {
console.log(“Enable iframe highlight”);
enableHighlight();
} else if (event.data.type === “disable-iframe-highlight”) {
console.log(“Disable iframe highlight”);
disableHighlight();
} else if (event.data.type === “toggle-iframe-highlight”) {
var _enabled = event.data.enabled !== undefined ? event.data.enabled : !highlightEnabled;
console.log(“Toggle iframe highlight to:”, _enabled);
if (_enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “update-selected-element”) {
var selector = event.data.selector;
console.log(“Update selected element with selector:”, selector);
if (selector) {
try {
var element = document.querySelector(selector);
console.log(“Found element by selector:”, element);
updateSelectedHighlight(element);
} catch (error) {
console.warn(“Failed to select element:”, error);
updateSelectedHighlight(null);
}
} else {
updateSelectedHighlight(null);
}
} else if (event.data.type === “clear-selected-element”) {
console.log(“Clear selected element”);
updateSelectedHighlight(null);
}
}

// 启用高亮功能
function enableHighlight() {
console.log(“Enabling highlight”);
document.addEventListener(“mouseover”, handleMouseOver, true);
document.addEventListener(“mouseout”, handleMouseOut, true);
document.addEventListener(“click”, handleClick, true);
highlightEnabled = true;
overlay.style.display = “block”;
}

// 禁用高亮功能
function disableHighlight() {
console.log(“Disabling highlight”);
highlightEnabled = false;
// 保持事件监听器,但通过 highlightEnabled 变量控制行为
// 这样可以保留选中状态的显示
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
// 不隐藏 selectedBox 和 selectedLabel,保留选中状态
}

// 完全禁用高亮功能(移除所有监听器)
function fullyDisableHighlight() {
console.log(“Fully disabling highlight”);
highlightEnabled = false;
document.removeEventListener(“mouseover”, handleMouseOver, true);
document.removeEventListener(“mouseout”, handleMouseOut, true);
document.removeEventListener(“click”, handleClick, true);
overlay.style.display = “none”;
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
}

// 添加事件监听
enableHighlight();
window.addEventListener(“message”, handleParentMessage);

// 暴露全局函数供外部调用
window.__iframeHighlightControl = {
enable: enableHighlight,
disable: disableHighlight,
fullyDisable: fullyDisableHighlight,
isEnabled: function isEnabled() {
return highlightEnabled;
},
getSelectedElement: function getSelectedElement() {
return selectedElement;
},
updateSelected: updateSelectedHighlight,
// 通过消息发送开关控制
sendToggleMessage: function sendToggleMessage(enabled) {
window.parent.postMessage({
type: ‘iframe-highlight-status’,
enabled: enabled || highlightEnabled,
source: ‘iframe-highlight-injector’
}, ‘*’);
}
};

// 通知父窗口脚本已加载
try {
window.parent.postMessage({
type: “iframe-highlight-ready”,
data: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
},
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送就绪消息到父窗口:”, error);
}

// 清理函数
window.__iframeHighlightCleanup = function () {
fullyDisableHighlight();
window.removeEventListener(“message”, handleParentMessage);
if (overlay.parentElement) {
overlay.parentElement.removeChild(overlay);
}
delete window.__iframeHighlightInitialized;
delete window.__iframeHighlightCleanup;
};
})();

</script>

<script>
/**
* Iframe 元素高亮注入脚本
* 需要在目标网站中引入此脚本来支持跨域 iframe 高亮功能
*
* 使用方法:
* 1. 将此脚本添加到目标网站的 HTML 中
* 2. 或通过浏览器扩展、用户脚本等方式注入
*/

(function () {
“use strict”;

// 检查是否在 iframe 中
if (window.self === window.top) {
return; // 不在 iframe 中,不执行
}

// 检查是否已经初始化过
if (window.__iframeHighlightInitialized) {
return;
}
window.__iframeHighlightInitialized = true;
console.log(“Iframe 高亮脚本已加载”);

// 创建高亮覆盖层
var overlay = document.createElement(“div”);
overlay.id = “iframe-highlight-overlay”;
overlay.style.cssText = “\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 999999;\n overflow: hidden;\n “;

// 创建悬停高亮框(虚线边框)
var highlightBox = document.createElement(“div”);
highlightBox.id = “iframe-highlight-box”;
highlightBox.style.cssText = “\n position: absolute;\n border: 2px dashed #007AFF;\n background: rgba(0, 122, 255, 0.08);\n pointer-events: none;\n display: none;\n transition: all 0.1s ease;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);\n border-radius: 2px;\n “;

// 创建选中节点的常驻高亮框(实线边框)
var selectedBox = document.createElement(“div”);
selectedBox.id = “iframe-selected-box”;
selectedBox.style.cssText = “\n position: absolute;\n border: 2px solid #007AFF;\n pointer-events: none;\n display: none;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 107, 53, 0.4);\n border-radius: 2px;\n z-index: 1000000;\n “;

// 创建悬停标签显示
var tagLabel = document.createElement(“div”);
tagLabel.id = “iframe-tag-label”;
tagLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 2px 6px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 2px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000001;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n font-weight: 500;\n “;

// 创建选中节点标签
var selectedLabel = document.createElement(“div”);
selectedLabel.id = “iframe-selected-label”;
selectedLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 3px 8px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 3px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000002;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);\n font-weight: 600;\n “;
overlay.appendChild(highlightBox);
overlay.appendChild(selectedBox);
overlay.appendChild(tagLabel);
overlay.appendChild(selectedLabel);
document.body.appendChild(overlay);

// 存储当前选中的元素
var selectedElement = null;
var highlightEnabled = false;

// 更新选中元素的高亮显示
function updateSelectedHighlight(element) {
console.log(“updateSelectedHighlight called with:”, element);
if (!element) {
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
selectedElement = null;
console.log(“Cleared selected highlight”);
return;
}
selectedElement = element;
var rect = element.getBoundingClientRect();
console.log(“Selected element rect:”, rect);

// 更新选中高亮框位置
selectedBox.style.display = “block”;
selectedBox.style.left = “”.concat(rect.left – 2, “px”);
selectedBox.style.top = “”.concat(rect.top – 2, “px”);
selectedBox.style.width = “”.concat(rect.width + 4, “px”);
selectedBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新选中标签位置和内容
selectedLabel.style.display = “block”;
selectedLabel.textContent = “\u2713 <“.concat(element.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 28;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 5) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
var labelWidth = selectedLabel.offsetWidth || 100; // 预估宽度
if (labelLeft + labelWidth > window.innerWidth – 10) {
labelLeft = window.innerWidth – labelWidth – 10;
}
selectedLabel.style.left = “”.concat(Math.max(5, labelLeft), “px”);
selectedLabel.style.top = “”.concat(labelTop, “px”);
console.log(“Selected highlight positioned at:”, {
left: selectedBox.style.left,
top: selectedBox.style.top,
width: selectedBox.style.width,
height: selectedBox.style.height
});
}
function getElementSelector(element) {
if (!(element instanceof Element)) throw new Error(‘Argument must be a DOM element’);
var segments = [];
var current = element;
while (current !== document.documentElement) {
var selector = ”;
// 优先检查唯一ID
if (current.id && document.querySelectorAll(“#”.concat(current.id)).length === 1) {
segments.unshift(“#”.concat(current.id));
break; // ID唯一,无需继续向上
}

// 生成类名选择器(取第一个有效类名)
var classes = Array.from(current.classList).filter(function (c) {
return !c.startsWith(‘js-‘);
});
var className = classes.length > 0 ? “.”.concat(classes[0]) : ”;

// 生成位置索引(nth-child)
var tag = current.tagName.toLowerCase();
if (!className) {
var siblings = Array.from(current.parentNode.children);
var index = siblings.findIndex(function (el) {
return el === current;
}) + 1;
selector = “”.concat(tag, “:nth-child(“).concat(index, “)”);
} else {
selector = className;
}
segments.unshift(selector);
current = current.parentElement;
}

// 处理根元素
if (current === document.documentElement) {
segments.unshift(‘html’);
}
return segments.join(‘ > ‘);
}

// 获取元素文本内容
function getElementText(element) {
var _element$textContent;
if (element.tagName === “INPUT”) {
return element.value || element.placeholder || “”;
}
if (element.tagName === “TEXTAREA”) {
return element.value || element.placeholder || “”;
}
var text = ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) || “”;
return text.length > 50 ? text.substring(0, 50) + “…” : text;
}

// 获取元素属性信息
function getElementAttributes(element) {
var attrs = {};
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
return attrs;
}

// 鼠标悬停事件处理
function handleMouseOver(e) {
if (!highlightEnabled) return;
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免高亮 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 如果是已选中的元素,不显示悬停高亮
if (target === selectedElement) {
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
return;
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);

// 更新悬停高亮框位置
highlightBox.style.display = “block”;
highlightBox.style.left = “”.concat(rect.left – 2, “px”);
highlightBox.style.top = “”.concat(rect.top – 2, “px”);
highlightBox.style.width = “”.concat(rect.width + 4, “px”);
highlightBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新标签位置和内容
tagLabel.style.display = “block”;
tagLabel.textContent = “<“.concat(target.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 22;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 0) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
if (labelLeft + tagLabel.offsetWidth > window.innerWidth) {
labelLeft = window.innerWidth – tagLabel.offsetWidth – 5;
}
tagLabel.style.left = “”.concat(Math.max(0, labelLeft), “px”);
tagLabel.style.top = “”.concat(labelTop, “px”);

// 发送消息到父窗口
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 鼠标离开事件处理
function handleMouseOut(e) {
if (!highlightEnabled) return;
var relatedTarget = e.relatedTarget;

// 如果鼠标移动到高亮相关元素上,不隐藏高亮
if (relatedTarget && (relatedTarget === highlightBox || relatedTarget === tagLabel || relatedTarget === overlay || relatedTarget === selectedBox || relatedTarget === selectedLabel)) {
return;
}
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: null,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 点击事件处理
function handleClick(e) {
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免处理 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 检查是否是交互元素,这些元素需要保留默认行为
var isInteractiveElement = [‘input’, ‘textarea’, ‘select’, ‘button’, ‘a’].includes(target.tagName.toLowerCase());

// 如果高亮功能启用,对于非交互元素阻止默认行为和事件传播
if (highlightEnabled) {
e.preventDefault();
e.stopPropagation();
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
console.log(“Element clicked:”, {
tagName: target.tagName,
selector: selector,
rect: rect
});

// 立即更新选中高亮
updateSelectedHighlight(target);

// 隐藏悬停高亮,因为现在是选中状态
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-click”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 监听来自父窗口的消息
function handleParentMessage(event) {
console.log(“Received message from parent:”, event.data);
if (event.data.type === “iframe-highlight-toggle”) {
var enabled = event.data.enabled;
console.log(“Highlight toggle:”, enabled);
if (enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “enable-iframe-highlight”) {
console.log(“Enable iframe highlight”);
enableHighlight();
} else if (event.data.type === “disable-iframe-highlight”) {
console.log(“Disable iframe highlight”);
disableHighlight();
} else if (event.data.type === “toggle-iframe-highlight”) {
var _enabled = event.data.enabled !== undefined ? event.data.enabled : !highlightEnabled;
console.log(“Toggle iframe highlight to:”, _enabled);
if (_enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “update-selected-element”) {
var selector = event.data.selector;
console.log(“Update selected element with selector:”, selector);
if (selector) {
try {
var element = document.querySelector(selector);
console.log(“Found element by selector:”, element);
updateSelectedHighlight(element);
} catch (error) {
console.warn(“Failed to select element:”, error);
updateSelectedHighlight(null);
}
} else {
updateSelectedHighlight(null);
}
} else if (event.data.type === “clear-selected-element”) {
console.log(“Clear selected element”);
updateSelectedHighlight(null);
}
}

// 启用高亮功能
function enableHighlight() {
console.log(“Enabling highlight”);
document.addEventListener(“mouseover”, handleMouseOver, true);
document.addEventListener(“mouseout”, handleMouseOut, true);
document.addEventListener(“click”, handleClick, true);
highlightEnabled = true;
overlay.style.display = “block”;
}

// 禁用高亮功能
function disableHighlight() {
console.log(“Disabling highlight”);
highlightEnabled = false;
// 保持事件监听器,但通过 highlightEnabled 变量控制行为
// 这样可以保留选中状态的显示
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
// 不隐藏 selectedBox 和 selectedLabel,保留选中状态
}

// 完全禁用高亮功能(移除所有监听器)
function fullyDisableHighlight() {
console.log(“Fully disabling highlight”);
highlightEnabled = false;
document.removeEventListener(“mouseover”, handleMouseOver, true);
document.removeEventListener(“mouseout”, handleMouseOut, true);
document.removeEventListener(“click”, handleClick, true);
overlay.style.display = “none”;
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
}

// 添加事件监听
enableHighlight();
window.addEventListener(“message”, handleParentMessage);

// 暴露全局函数供外部调用
window.__iframeHighlightControl = {
enable: enableHighlight,
disable: disableHighlight,
fullyDisable: fullyDisableHighlight,
isEnabled: function isEnabled() {
return highlightEnabled;
},
getSelectedElement: function getSelectedElement() {
return selectedElement;
},
updateSelected: updateSelectedHighlight,
// 通过消息发送开关控制
sendToggleMessage: function sendToggleMessage(enabled) {
window.parent.postMessage({
type: ‘iframe-highlight-status’,
enabled: enabled || highlightEnabled,
source: ‘iframe-highlight-injector’
}, ‘*’);
}
};

// 通知父窗口脚本已加载
try {
window.parent.postMessage({
type: “iframe-highlight-ready”,
data: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
},
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送就绪消息到父窗口:”, error);
}

// 清理函数
window.__iframeHighlightCleanup = function () {
fullyDisableHighlight();
window.removeEventListener(“message”, handleParentMessage);
if (overlay.parentElement) {
overlay.parentElement.removeChild(overlay);
}
delete window.__iframeHighlightInitialized;
delete window.__iframeHighlightCleanup;
};
})();

</script>

<script>
/**
* Iframe 元素高亮注入脚本
* 需要在目标网站中引入此脚本来支持跨域 iframe 高亮功能
*
* 使用方法:
* 1. 将此脚本添加到目标网站的 HTML 中
* 2. 或通过浏览器扩展、用户脚本等方式注入
*/

(function () {
“use strict”;

// 检查是否在 iframe 中
if (window.self === window.top) {
return; // 不在 iframe 中,不执行
}

// 检查是否已经初始化过
if (window.__iframeHighlightInitialized) {
return;
}
window.__iframeHighlightInitialized = true;
console.log(“Iframe 高亮脚本已加载”);

// 创建高亮覆盖层
var overlay = document.createElement(“div”);
overlay.id = “iframe-highlight-overlay”;
overlay.style.cssText = “\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n pointer-events: none;\n z-index: 999999;\n overflow: hidden;\n “;

// 创建悬停高亮框(虚线边框)
var highlightBox = document.createElement(“div”);
highlightBox.id = “iframe-highlight-box”;
highlightBox.style.cssText = “\n position: absolute;\n border: 2px dashed #007AFF;\n background: rgba(0, 122, 255, 0.08);\n pointer-events: none;\n display: none;\n transition: all 0.1s ease;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.8);\n border-radius: 2px;\n “;

// 创建选中节点的常驻高亮框(实线边框)
var selectedBox = document.createElement(“div”);
selectedBox.id = “iframe-selected-box”;
selectedBox.style.cssText = “\n position: absolute;\n border: 2px solid #007AFF;\n pointer-events: none;\n display: none;\n box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.9), 0 0 8px rgba(255, 107, 53, 0.4);\n border-radius: 2px;\n z-index: 1000000;\n “;

// 创建悬停标签显示
var tagLabel = document.createElement(“div”);
tagLabel.id = “iframe-tag-label”;
tagLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 2px 6px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 2px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000001;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);\n font-weight: 500;\n “;

// 创建选中节点标签
var selectedLabel = document.createElement(“div”);
selectedLabel.id = “iframe-selected-label”;
selectedLabel.style.cssText = “\n position: absolute;\n background: #007AFF;\n color: white;\n padding: 3px 8px;\n font-size: 11px;\n font-family: -apple-system, BlinkMacSystemFont, ‘Segoe UI’, Roboto, ‘Helvetica Neue’, Arial, sans-serif;\n border-radius: 3px;\n pointer-events: none;\n display: none;\n white-space: nowrap;\n z-index: 1000002;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.4);\n font-weight: 600;\n “;
overlay.appendChild(highlightBox);
overlay.appendChild(selectedBox);
overlay.appendChild(tagLabel);
overlay.appendChild(selectedLabel);
document.body.appendChild(overlay);

// 存储当前选中的元素
var selectedElement = null;
var highlightEnabled = false;

// 更新选中元素的高亮显示
function updateSelectedHighlight(element) {
console.log(“updateSelectedHighlight called with:”, element);
if (!element) {
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
selectedElement = null;
console.log(“Cleared selected highlight”);
return;
}
selectedElement = element;
var rect = element.getBoundingClientRect();
console.log(“Selected element rect:”, rect);

// 更新选中高亮框位置
selectedBox.style.display = “block”;
selectedBox.style.left = “”.concat(rect.left – 2, “px”);
selectedBox.style.top = “”.concat(rect.top – 2, “px”);
selectedBox.style.width = “”.concat(rect.width + 4, “px”);
selectedBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新选中标签位置和内容
selectedLabel.style.display = “block”;
selectedLabel.textContent = “\u2713 <“.concat(element.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 28;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 5) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
var labelWidth = selectedLabel.offsetWidth || 100; // 预估宽度
if (labelLeft + labelWidth > window.innerWidth – 10) {
labelLeft = window.innerWidth – labelWidth – 10;
}
selectedLabel.style.left = “”.concat(Math.max(5, labelLeft), “px”);
selectedLabel.style.top = “”.concat(labelTop, “px”);
console.log(“Selected highlight positioned at:”, {
left: selectedBox.style.left,
top: selectedBox.style.top,
width: selectedBox.style.width,
height: selectedBox.style.height
});
}
function getElementSelector(element) {
if (!(element instanceof Element)) throw new Error(‘Argument must be a DOM element’);
var segments = [];
var current = element;
while (current !== document.documentElement) {
var selector = ”;
// 优先检查唯一ID
if (current.id && document.querySelectorAll(“#”.concat(current.id)).length === 1) {
segments.unshift(“#”.concat(current.id));
break; // ID唯一,无需继续向上
}

// 生成类名选择器(取第一个有效类名)
var classes = Array.from(current.classList).filter(function (c) {
return !c.startsWith(‘js-‘);
});
var className = classes.length > 0 ? “.”.concat(classes[0]) : ”;

// 生成位置索引(nth-child)
var tag = current.tagName.toLowerCase();
if (!className) {
var siblings = Array.from(current.parentNode.children);
var index = siblings.findIndex(function (el) {
return el === current;
}) + 1;
selector = “”.concat(tag, “:nth-child(“).concat(index, “)”);
} else {
selector = className;
}
segments.unshift(selector);
current = current.parentElement;
}

// 处理根元素
if (current === document.documentElement) {
segments.unshift(‘html’);
}
return segments.join(‘ > ‘);
}

// 获取元素文本内容
function getElementText(element) {
var _element$textContent;
if (element.tagName === “INPUT”) {
return element.value || element.placeholder || “”;
}
if (element.tagName === “TEXTAREA”) {
return element.value || element.placeholder || “”;
}
var text = ((_element$textContent = element.textContent) === null || _element$textContent === void 0 ? void 0 : _element$textContent.trim()) || “”;
return text.length > 50 ? text.substring(0, 50) + “…” : text;
}

// 获取元素属性信息
function getElementAttributes(element) {
var attrs = {};
for (var i = 0; i < element.attributes.length; i++) {
var attr = element.attributes[i];
attrs[attr.name] = attr.value;
}
return attrs;
}

// 鼠标悬停事件处理
function handleMouseOver(e) {
if (!highlightEnabled) return;
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免高亮 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 如果是已选中的元素,不显示悬停高亮
if (target === selectedElement) {
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
return;
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);

// 更新悬停高亮框位置
highlightBox.style.display = “block”;
highlightBox.style.left = “”.concat(rect.left – 2, “px”);
highlightBox.style.top = “”.concat(rect.top – 2, “px”);
highlightBox.style.width = “”.concat(rect.width + 4, “px”);
highlightBox.style.height = “”.concat(rect.height + 4, “px”);

// 更新标签位置和内容
tagLabel.style.display = “block”;
tagLabel.textContent = “<“.concat(target.tagName.toLowerCase(), “>”);

// 计算标签位置,确保不超出视窗
var labelTop = rect.top – 22;
var labelLeft = rect.left;

// 如果标签会超出顶部,显示在元素下方
if (labelTop < 0) {
labelTop = rect.bottom + 5;
}

// 如果标签会超出右侧,向左调整
if (labelLeft + tagLabel.offsetWidth > window.innerWidth) {
labelLeft = window.innerWidth – tagLabel.offsetWidth – 5;
}
tagLabel.style.left = “”.concat(Math.max(0, labelLeft), “px”);
tagLabel.style.top = “”.concat(labelTop, “px”);

// 发送消息到父窗口
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 鼠标离开事件处理
function handleMouseOut(e) {
if (!highlightEnabled) return;
var relatedTarget = e.relatedTarget;

// 如果鼠标移动到高亮相关元素上,不隐藏高亮
if (relatedTarget && (relatedTarget === highlightBox || relatedTarget === tagLabel || relatedTarget === overlay || relatedTarget === selectedBox || relatedTarget === selectedLabel)) {
return;
}
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
try {
window.parent.postMessage({
type: “iframe-element-hover”,
data: null,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 点击事件处理
function handleClick(e) {
var target = e.target;
if (!target || target === overlay || target === highlightBox || target === tagLabel || target === selectedBox || target === selectedLabel) {
return;
}

// 避免处理 html 和 body 元素
if (target === document.documentElement || target === document.body) {
return;
}

// 检查是否是交互元素,这些元素需要保留默认行为
var isInteractiveElement = [‘input’, ‘textarea’, ‘select’, ‘button’, ‘a’].includes(target.tagName.toLowerCase());

// 如果高亮功能启用,对于非交互元素阻止默认行为和事件传播
if (highlightEnabled) {
e.preventDefault();
e.stopPropagation();
}
var rect = target.getBoundingClientRect();
var selector = getElementSelector(target);
var text = getElementText(target);
var attributes = getElementAttributes(target);
console.log(“Element clicked:”, {
tagName: target.tagName,
selector: selector,
rect: rect
});

// 立即更新选中高亮
updateSelectedHighlight(target);

// 隐藏悬停高亮,因为现在是选中状态
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
var elementInfo = {
tagName: target.tagName.toLowerCase(),
rect: {
left: rect.left,
top: rect.top,
right: rect.right,
bottom: rect.bottom,
width: rect.width,
height: rect.height,
x: rect.x,
y: rect.y
},
selector: selector,
text: text,
attributes: attributes,
url: window.location.href,
path: window.location.pathname,
timestamp: Date.now()
};
try {
window.parent.postMessage({
type: “iframe-element-click”,
data: elementInfo,
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送消息到父窗口:”, error);
}
}

// 监听来自父窗口的消息
function handleParentMessage(event) {
console.log(“Received message from parent:”, event.data);
if (event.data.type === “iframe-highlight-toggle”) {
var enabled = event.data.enabled;
console.log(“Highlight toggle:”, enabled);
if (enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “enable-iframe-highlight”) {
console.log(“Enable iframe highlight”);
enableHighlight();
} else if (event.data.type === “disable-iframe-highlight”) {
console.log(“Disable iframe highlight”);
disableHighlight();
} else if (event.data.type === “toggle-iframe-highlight”) {
var _enabled = event.data.enabled !== undefined ? event.data.enabled : !highlightEnabled;
console.log(“Toggle iframe highlight to:”, _enabled);
if (_enabled) {
enableHighlight();
} else {
disableHighlight();
}
} else if (event.data.type === “update-selected-element”) {
var selector = event.data.selector;
console.log(“Update selected element with selector:”, selector);
if (selector) {
try {
var element = document.querySelector(selector);
console.log(“Found element by selector:”, element);
updateSelectedHighlight(element);
} catch (error) {
console.warn(“Failed to select element:”, error);
updateSelectedHighlight(null);
}
} else {
updateSelectedHighlight(null);
}
} else if (event.data.type === “clear-selected-element”) {
console.log(“Clear selected element”);
updateSelectedHighlight(null);
}
}

// 启用高亮功能
function enableHighlight() {
console.log(“Enabling highlight”);
document.addEventListener(“mouseover”, handleMouseOver, true);
document.addEventListener(“mouseout”, handleMouseOut, true);
document.addEventListener(“click”, handleClick, true);
highlightEnabled = true;
overlay.style.display = “block”;
}

// 禁用高亮功能
function disableHighlight() {
console.log(“Disabling highlight”);
highlightEnabled = false;
// 保持事件监听器,但通过 highlightEnabled 变量控制行为
// 这样可以保留选中状态的显示
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
// 不隐藏 selectedBox 和 selectedLabel,保留选中状态
}

// 完全禁用高亮功能(移除所有监听器)
function fullyDisableHighlight() {
console.log(“Fully disabling highlight”);
highlightEnabled = false;
document.removeEventListener(“mouseover”, handleMouseOver, true);
document.removeEventListener(“mouseout”, handleMouseOut, true);
document.removeEventListener(“click”, handleClick, true);
overlay.style.display = “none”;
highlightBox.style.display = “none”;
tagLabel.style.display = “none”;
selectedBox.style.display = “none”;
selectedLabel.style.display = “none”;
}

// 添加事件监听
enableHighlight();
window.addEventListener(“message”, handleParentMessage);

// 暴露全局函数供外部调用
window.__iframeHighlightControl = {
enable: enableHighlight,
disable: disableHighlight,
fullyDisable: fullyDisableHighlight,
isEnabled: function isEnabled() {
return highlightEnabled;
},
getSelectedElement: function getSelectedElement() {
return selectedElement;
},
updateSelected: updateSelectedHighlight,
// 通过消息发送开关控制
sendToggleMessage: function sendToggleMessage(enabled) {
window.parent.postMessage({
type: ‘iframe-highlight-status’,
enabled: enabled || highlightEnabled,
source: ‘iframe-highlight-injector’
}, ‘*’);
}
};

// 通知父窗口脚本已加载
try {
window.parent.postMessage({
type: “iframe-highlight-ready”,
data: {
url: window.location.href,
userAgent: navigator.userAgent,
timestamp: Date.now()
},
source: “iframe-highlight-injector”
}, “*”);
} catch (error) {
console.warn(“无法发送就绪消息到父窗口:”, error);
}

// 清理函数
window.__iframeHighlightCleanup = function () {
fullyDisableHighlight();
window.removeEventListener(“message”, handleParentMessage);
if (overlay.parentElement) {
overlay.parentElement.removeChild(overlay);
}
delete window.__iframeHighlightInitialized;
delete window.__iframeHighlightCleanup;
};
})();

</script>
</body>
</html>