继续更新

This commit is contained in:
2025-10-15 11:11:23 +08:00
parent 7786e5f507
commit c147502b4d
89 changed files with 8116 additions and 2505 deletions

View File

@@ -0,0 +1,79 @@
.background-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: -1;
overflow: hidden;
background: linear-gradient(180deg, #f0f9f2 0%, #f7fff8 55%, #eef7f1 100%);
}
.floating-blob {
position: absolute;
width: 420px;
height: 420px;
border-radius: 55% 45% 60% 40% / 50% 50% 45% 55%;
filter: blur(0px);
opacity: 0.28;
background: radial-gradient(circle at 30% 30%, rgba(129, 199, 132, 0.6), rgba(129, 199, 132, 0));
animation: drift 36s ease-in-out infinite;
}
.blob-1 {
top: -120px;
left: -160px;
animation-delay: 0s;
}
.blob-2 {
right: -120px;
bottom: -160px;
animation-delay: 8s;
background: radial-gradient(circle at 70% 70%, rgba(76, 175, 80, 0.5), rgba(76, 175, 80, 0));
}
.blob-3 {
top: 45%;
left: 55%;
animation-delay: 16s;
background: radial-gradient(circle at 40% 60%, rgba(165, 214, 167, 0.5), rgba(165, 214, 167, 0));
}
@keyframes drift {
0% {
transform: translate3d(0, 0, 0) scale(1);
}
33% {
transform: translate3d(30px, -40px, 0) scale(1.05);
}
66% {
transform: translate3d(-25px, 30px, 0) scale(0.95);
}
100% {
transform: translate3d(0, 0, 0) scale(1);
}
}
@media (max-width: 768px) {
.floating-blob {
width: 260px;
height: 260px;
opacity: 0.22;
}
.blob-1 {
top: -80px;
left: -120px;
}
.blob-2 {
right: -140px;
bottom: -140px;
}
.blob-3 {
top: 55%;
left: 48%;
}
}

View File

@@ -0,0 +1,432 @@
:root {
--primary-50: #f0f9f2;
--primary-100: #d8f3d8;
--primary-200: #bce5c1;
--primary-300: #a0d8a8;
--primary-400: #7fcf8e;
--primary-500: #66bb6a;
--primary-600: #5aa75f;
--primary-700: #4a8c50;
--primary-text: #103a2b;
--muted-text: #49705d;
--card-bg: rgba(255, 255, 255, 0.85);
--border-soft: rgba(102, 187, 106, 0.18);
--shadow-soft: 0 12px 40px rgba(48, 94, 60, 0.12);
--shadow-hover: 0 18px 50px rgba(48, 94, 60, 0.16);
color-scheme: light;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: "PingFang SC", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif;
background: transparent;
color: var(--primary-text);
line-height: 1.6;
min-height: 100vh;
-webkit-font-smoothing: antialiased;
}
.page {
position: relative;
margin: 0 auto;
width: min(100%, 960px);
padding: 20px 16px 72px;
}
.page-header {
display: flex;
flex-direction: column;
gap: 16px;
padding: 20px 18px;
border-radius: 18px;
background: var(--card-bg);
box-shadow: var(--shadow-soft);
border: 1px solid var(--border-soft);
backdrop-filter: blur(18px);
}
.title-block {
display: flex;
flex-direction: column;
gap: 6px;
}
.label-pill {
align-self: flex-start;
padding: 4px 12px;
font-size: 0.82rem;
font-weight: 600;
color: var(--primary-700);
background: rgba(102, 187, 106, 0.15);
border-radius: 999px;
letter-spacing: 0.06em;
}
.page-header h1 {
font-size: 1.6rem;
font-weight: 700;
line-height: 1.3;
}
.meta-block {
display: flex;
flex-direction: column;
gap: 10px;
}
.update-time {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
font-size: 0.9rem;
color: var(--muted-text);
background: rgba(255, 255, 255, 0.7);
border-radius: 10px;
border: 1px solid rgba(102, 187, 106, 0.22);
}
.refresh-button {
align-self: flex-start;
padding: 10px 18px;
font-size: 0.92rem;
font-weight: 600;
color: #fff;
background: linear-gradient(135deg, var(--primary-500), var(--primary-600));
border: none;
border-radius: 999px;
cursor: pointer;
box-shadow: 0 10px 24px rgba(102, 187, 106, 0.35);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.refresh-button:active {
transform: scale(0.97);
}
.summary-section {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 14px;
margin-top: 20px;
}
.summary-card {
padding: 18px 16px;
background: var(--card-bg);
border-radius: 16px;
border: 1px solid var(--border-soft);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(12px);
display: flex;
flex-direction: column;
gap: 8px;
}
.card-label {
font-size: 0.92rem;
color: var(--muted-text);
letter-spacing: 0.04em;
}
.card-value {
font-size: 1.4rem;
font-weight: 700;
display: flex;
align-items: baseline;
gap: 6px;
color: var(--primary-700);
}
.card-value .unit {
font-size: 0.88rem;
font-weight: 500;
color: var(--muted-text);
}
.list-section {
margin-top: 28px;
padding: 22px 18px 26px;
background: var(--card-bg);
border-radius: 20px;
border: 1px solid var(--border-soft);
box-shadow: var(--shadow-soft);
backdrop-filter: blur(16px);
}
.section-header {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 20px;
}
.section-header h2 {
font-size: 1.3rem;
font-weight: 700;
}
.section-subtitle {
font-size: 0.88rem;
color: var(--muted-text);
}
.movie-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.loading,
.error-message {
padding: 18px 16px;
text-align: center;
color: var(--muted-text);
background: rgba(255, 255, 255, 0.7);
border-radius: 14px;
border: 1px dashed rgba(102, 187, 106, 0.35);
}
.movie-item {
display: grid;
grid-template-columns: auto 1fr;
gap: 14px;
padding: 16px;
border-radius: 18px;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(102, 187, 106, 0.18);
box-shadow: 0 12px 28px rgba(48, 94, 60, 0.08);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.movie-item:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-hover);
}
.movie-rank {
width: 46px;
height: 46px;
border-radius: 14px;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 1.1rem;
color: #fff;
background: var(--primary-500);
}
.movie-rank.top-1 {
background: linear-gradient(135deg, #4caf50, #43a047);
}
.movie-rank.top-2 {
background: linear-gradient(135deg, #66bb6a, #5aa75f);
}
.movie-rank.top-3 {
background: linear-gradient(135deg, #81c784, #66bb6a);
}
.movie-body {
display: flex;
flex-direction: column;
gap: 12px;
}
.movie-heading {
display: flex;
flex-direction: column;
gap: 6px;
}
.movie-title {
font-size: 1.1rem;
font-weight: 700;
color: var(--primary-text);
}
.release-info {
font-size: 0.9rem;
color: var(--muted-text);
}
.movie-stats {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px 14px;
}
.stat {
display: flex;
flex-direction: column;
gap: 2px;
}
.stat-label {
font-size: 0.83rem;
color: var(--muted-text);
letter-spacing: 0.02em;
}
.stat-value {
font-size: 1rem;
font-weight: 600;
color: var(--primary-700);
}
.progress-metrics {
display: flex;
flex-direction: column;
gap: 8px;
margin-top: 6px;
}
.progress-group {
display: flex;
flex-direction: column;
gap: 4px;
}
.progress-label {
display: flex;
justify-content: space-between;
font-size: 0.8rem;
color: var(--muted-text);
}
.progress-bar {
width: 100%;
height: 6px;
border-radius: 6px;
background: rgba(102, 187, 106, 0.16);
overflow: hidden;
}
.progress-bar span {
display: block;
height: 100%;
border-radius: inherit;
background: linear-gradient(135deg, rgba(102, 187, 106, 0.9), rgba(76, 175, 80, 0.85));
width: 0;
transition: width 0.5s ease;
}
/* Tablet */
@media (min-width: 600px) {
.page {
padding: 28px 20px 84px;
}
.page-header {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.title-block h1 {
font-size: 1.8rem;
}
.meta-block {
align-items: flex-end;
}
.summary-section {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.movie-item {
grid-template-columns: 80px 1fr;
padding: 18px 20px;
}
.movie-rank {
width: 54px;
height: 54px;
font-size: 1.25rem;
}
.movie-heading {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.release-info {
font-size: 0.88rem;
}
.movie-stats {
grid-template-columns: repeat(4, minmax(0, 1fr));
}
.progress-metrics {
flex-direction: row;
gap: 16px;
}
.progress-group {
flex: 1;
}
}
/* Desktop */
@media (min-width: 1024px) {
.page {
padding: 36px 24px 96px;
}
.page-header {
padding: 26px 30px;
}
.page-header h1 {
font-size: 2.1rem;
}
.summary-card {
padding: 20px 22px;
}
.card-value {
font-size: 1.6rem;
}
.list-section {
padding: 28px 30px 34px;
}
.movie-item {
grid-template-columns: 96px 1fr;
padding: 22px 26px;
border-radius: 20px;
}
.movie-title {
font-size: 1.25rem;
}
.movie-stats {
grid-template-columns: repeat(5, minmax(0, 1fr));
}
.progress-metrics {
gap: 18px;
}
}
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}

View File

@@ -0,0 +1,67 @@
<!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 rel="stylesheet" href="./css/background.css">
<link rel="stylesheet" href="./css/style.css">
</head>
<body>
<div class="background-container">
<div class="floating-blob blob-1"></div>
<div class="floating-blob blob-2"></div>
<div class="floating-blob blob-3"></div>
</div>
<div class="page">
<header class="page-header">
<div class="title-block">
<span class="label-pill">实时票房</span>
<h1>猫眼电影实时票房</h1>
</div>
<div class="meta-block">
<span id="updateTime" class="update-time">正在获取最新更新...</span>
<button type="button" id="refreshButton" class="refresh-button">手动刷新</button>
</div>
</header>
<section class="summary-section" id="summarySection">
<div class="summary-card">
<p class="card-label" id="summaryTitle">实时大盘</p>
<p class="card-value">
<span id="totalBoxOffice">--</span>
<span class="unit" id="totalBoxOfficeUnit"></span>
</p>
</div>
<div class="summary-card">
<p class="card-label">综合票房</p>
<p class="card-value">
<span id="combinedBoxOffice">--</span>
<span class="unit" id="combinedBoxOfficeUnit"></span>
</p>
</div>
<div class="summary-card">
<p class="card-label">排片场次</p>
<p class="card-value" id="showCount">--</p>
</div>
<div class="summary-card">
<p class="card-label">观影人次</p>
<p class="card-value" id="viewCount">--</p>
</div>
</section>
<section class="list-section">
<div class="section-header">
<h2>影片实时表现</h2>
<span class="section-subtitle">数据每 5 秒同步一次</span>
</div>
<div id="movieList" class="movie-list">
<div class="loading">正在加载实时票房...</div>
</div>
</section>
</div>
<script src="./js/main.js"></script>
</body>
</html>

View File

@@ -0,0 +1,297 @@
const API_ENDPOINTS = [
"https://60s.api.shumengya.top/v2/maoyan/realtime/movie"
];
const FALLBACK_ENDPOINT = "./返回接口.json";
const REFRESH_INTERVAL = 5000;
const MAX_MOVIES_TO_RENDER = 40;
const updateTimeEl = document.getElementById("updateTime");
const refreshButton = document.getElementById("refreshButton");
const summaryTitleEl = document.getElementById("summaryTitle");
const totalBoxOfficeEl = document.getElementById("totalBoxOffice");
const totalBoxOfficeUnitEl = document.getElementById("totalBoxOfficeUnit");
const combinedBoxOfficeEl = document.getElementById("combinedBoxOffice");
const combinedBoxOfficeUnitEl = document.getElementById("combinedBoxOfficeUnit");
const showCountEl = document.getElementById("showCount");
const viewCountEl = document.getElementById("viewCount");
const movieListEl = document.getElementById("movieList");
let autoRefreshTimer = null;
let isLoading = false;
function escapeHtml(value) {
if (value === undefined || value === null) {
return "";
}
return String(value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function safeText(value) {
if (value === undefined || value === null || value === "") {
return "--";
}
return escapeHtml(value);
}
function parseRate(rateText) {
if (!rateText || typeof rateText !== "string") {
return { text: "--", ratio: 0 };
}
const trimmed = rateText.trim();
const numeric = parseFloat(trimmed.replace(/[^0-9.]/g, ""));
let ratio = Number.isFinite(numeric) ? Math.max(0, Math.min(numeric, 100)) : 0;
if (trimmed.startsWith("<")) {
ratio = Math.max(3, ratio);
}
return { text: escapeHtml(trimmed), ratio };
}
function formatUpdateTime(data) {
if (data && typeof data.updated === "string" && data.updated.trim().length > 0) {
return data.updated.trim();
}
if (data && typeof data.updated_at === "number" && !Number.isNaN(data.updated_at)) {
return new Date(data.updated_at).toLocaleString("zh-CN", {
hour12: false
});
}
return new Date().toLocaleString("zh-CN", { hour12: false });
}
function renderSummary(data) {
summaryTitleEl.textContent = data?.title ? data.title : "实时大盘";
totalBoxOfficeEl.textContent = data?.split_box_office ? data.split_box_office : "--";
totalBoxOfficeUnitEl.textContent = data?.split_box_office_unit ? data.split_box_office_unit : "";
combinedBoxOfficeEl.textContent = data?.box_office ? data.box_office : "--";
combinedBoxOfficeUnitEl.textContent = data?.box_office_unit ? data.box_office_unit : "";
showCountEl.textContent = data?.show_count_desc ? data.show_count_desc : "--";
viewCountEl.textContent = data?.view_count_desc ? data.view_count_desc : "--";
}
function createStat(label, value) {
return `
<div class="stat">
<span class="stat-label">${label}</span>
<span class="stat-value">${safeText(value)}</span>
</div>
`;
}
function createMovieItem(movie, index) {
const item = document.createElement("article");
item.className = "movie-item";
const topClass = index < 3 ? ` top-${index + 1}` : "";
const name = safeText(movie?.movie_name || "未命名影片");
const releaseInfo = movie?.release_info ? `<div class="release-info">${safeText(movie.release_info)}</div>` : "";
const boxOfficeDesc = movie?.box_office_desc || (movie?.box_office ? `${movie.box_office}${movie.box_office_unit || ""}` : "--");
const splitBoxOfficeDesc = movie?.split_box_office_desc || (movie?.split_box_office ? `${movie.split_box_office}${movie.split_box_office_unit || ""}` : "--");
const totalBoxOfficeDesc = movie?.sum_box_desc ?? "--";
const totalSplitBoxOfficeDesc = movie?.sum_split_box_desc ?? "--";
let showCountText = "--";
if (movie?.show_count !== undefined && movie.show_count !== null && movie.show_count !== "") {
const numericShowCount = Number(movie.show_count);
showCountText = Number.isFinite(numericShowCount)
? `${numericShowCount.toLocaleString("zh-CN")}`
: movie.show_count;
}
const avgShowView = movie?.avg_show_view ?? "--";
const avgSeatView = movie?.avg_seat_view ?? "--";
const boxRate = parseRate(movie?.box_office_rate);
const showRate = parseRate(movie?.show_count_rate);
item.innerHTML = `
<div class="movie-rank${topClass}">${index + 1}</div>
<div class="movie-body">
<div class="movie-heading">
<div>
<div class="movie-title">${name}</div>
${releaseInfo}
</div>
</div>
<div class="movie-stats">
${createStat("单日综合票房", boxOfficeDesc)}
${createStat("单日分账票房", splitBoxOfficeDesc)}
${createStat("累计综合票房", totalBoxOfficeDesc)}
${createStat("累计分账票房", totalSplitBoxOfficeDesc)}
${createStat("排片场次", showCountText)}
${createStat("场均人次", avgShowView)}
${createStat("上座率", avgSeatView)}
</div>
<div class="progress-metrics">
<div class="progress-group">
<div class="progress-label">
<span>综合票房占比</span>
<span>${boxRate.text}</span>
</div>
<div class="progress-bar"><span></span></div>
</div>
<div class="progress-group">
<div class="progress-label">
<span>排片占比</span>
<span>${showRate.text}</span>
</div>
<div class="progress-bar"><span></span></div>
</div>
</div>
</div>
`;
const progressBars = item.querySelectorAll(".progress-bar span");
if (progressBars[0]) {
progressBars[0].style.width = `${boxRate.ratio}%`;
}
if (progressBars[1]) {
progressBars[1].style.width = `${showRate.ratio}%`;
}
return item;
}
function renderMovieList(list) {
movieListEl.innerHTML = "";
if (!Array.isArray(list) || list.length === 0) {
const empty = document.createElement("div");
empty.className = "error-message";
empty.textContent = "暂时没有可展示的实时票房数据";
movieListEl.appendChild(empty);
return;
}
const sliced = list.slice(0, MAX_MOVIES_TO_RENDER);
sliced.forEach((movie, index) => {
movieListEl.appendChild(createMovieItem(movie, index));
});
}
async function requestJson(url) {
const response = await fetch(url, {
cache: "no-store"
});
if (!response.ok) {
throw new Error(`请求失败: ${response.status}`);
}
return response.json();
}
async function retrieveData() {
for (const endpoint of API_ENDPOINTS) {
try {
const result = await requestJson(endpoint);
if (result?.code === 200 && result?.data) {
return result.data;
}
} catch (error) {
console.warn("主接口请求失败", error);
}
}
try {
const fallbackResult = await requestJson(FALLBACK_ENDPOINT);
if (fallbackResult?.data) {
return fallbackResult.data;
}
} catch (error) {
console.warn("本地示例数据读取失败", error);
}
return null;
}
async function loadData(isManual = false) {
if (isLoading) {
return;
}
isLoading = true;
if (isManual) {
refreshButton.disabled = true;
refreshButton.textContent = "刷新中...";
}
if (!movieListEl.children.length) {
movieListEl.innerHTML = '<div class="loading">正在加载实时票房...</div>';
}
try {
const data = await retrieveData();
if (!data) {
throw new Error("无法获取数据");
}
renderSummary(data);
renderMovieList(Array.isArray(data.list) ? data.list : []);
updateTimeEl.textContent = `最近更新 ${formatUpdateTime(data)}`;
} catch (error) {
console.error("加载数据失败", error);
movieListEl.innerHTML = "";
const err = document.createElement("div");
err.className = "error-message";
err.textContent = "数据获取暂时遇到问题,系统会稍后自动重试";
movieListEl.appendChild(err);
updateTimeEl.textContent = "最近更新 --";
renderSummary(null);
} finally {
if (isManual) {
refreshButton.disabled = false;
refreshButton.textContent = "手动刷新";
}
isLoading = false;
}
}
function startAutoRefresh() {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
}
autoRefreshTimer = setInterval(() => {
loadData(false);
}, REFRESH_INTERVAL);
}
refreshButton.addEventListener("click", () => {
loadData(true);
});
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
if (autoRefreshTimer) {
clearInterval(autoRefreshTimer);
autoRefreshTimer = null;
}
} else {
startAutoRefresh();
loadData(false);
}
});
function init() {
loadData(false);
startAutoRefresh();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}