feat: 添加HEVC转H.264播放器和打字机效果页面

添加HEVC转H.264播放器功能,包含文件选择、转码进度显示和视频播放
新增打字机效果展示页面,使用Vue实现逐字显示效果
This commit is contained in:
DESKTOP-RQ919RC\Pc
2025-07-25 18:49:11 +08:00
parent 15cf9c3041
commit 047ddd57f6
4 changed files with 778 additions and 181 deletions

55
1.html
View File

@@ -1,26 +1,49 @@
<!DOCTYPE html>
<html lang="en">
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>EasyWasmPlayer-Demo</title>
<script src="./EasyWasmPlayer.js"></script>
<title>HEVC转H.264播放器</title>
<style>
.box {
width: 600px;
height: 400px;
.container {
max-width: 1000px;
margin: 20px auto;
padding: 20px;
}
.progress-container {
height: 8px;
background: #eee;
border-radius: 4px;
margin: 10px 0;
}
#progressBar {
height: 100%;
background: #4285f4;
width: 0%;
}
#videoContainer {
width: 100%;
background: #000;
border-radius: 8px;
overflow: hidden;
position: relative;
min-height: 300px;
}
video {
width: 100%;
}
</style>
</head>
<body>
<div class="box">
<div id="Player"></div>
<div class="container">
<h1>HEVC转H.264播放器</h1>
<input type="file" id="hevcFileInput" accept="video/*" />
<div id="conversionStatus">请选择HEVC编码的视频文件</div>
<div class="progress-container">
<div id="progressBar"></div>
</div>
<div id="videoContainer"></div>
</div>
<script>
// 实例化播放器
var Player = new WasmPlayer(null, "Player", callbackFun, { cbUserPtr: this, decodeType: "auto", openAudio: 1, BigPlay: false, Height: true });
// 调用播放
Player.play("url", 1);
</script>
<!-- 引入核心脚本 -->
<script src="hevc-to-h264-player.js"></script>
</body>
</html>

88
2.html Normal file
View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>打字机效果</title>
<style>
.flex {
display: flex;
}
.flex-1 {
flex: 1;
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.7.8/vue.min.js"></script>
</head>
<body>
<div id="app">
<div v-for="(item, index) in clientpreferenceVOListCopy">
<div class="flex">
<div>
{{ item.type }}
</div>:
<div class="flex-1" v-html="item.summary"></div>
</div>
</div>
</div>
<script>
var app = new Vue({
el: '#app',
data: {
clientpreferenceVOList: [
{ "type": "意外", "summary": "虽然客户目前为赋闲大众但生活中总会有意外发生。根据中国保险监督管理委员会的数据2019年中国交通事故死亡人数约为3.7万人平均每天有877人因交通事故而丧生。意外风险不仅包括交通事故还包括家庭意外、职业意外等。考虑到客户目前为单身贵族可能需要承担更多的家庭责任因此意外风险不容忽视。购买意外险可以为客户提供意外伤害的保障确保在意外发生时能够得到及时的经济支持。" },
{ "type": "医疗", "summary": "客户目前无社保意味着在医疗方面可能会面临较大的经济压力。根据中国保险监督管理委员会的数据2019年中国医疗费用总支出约为6.5万亿元其中个人自付比例约为20%,而客户作为单身贵族,可能需要承担更多的自付费用。医疗风险不仅包括治疗费用,还包括药物费用、检查费用、住院费用等。因此,购买医疗保险对于客户来说至关重要,可以有效减轻因疾病带来的经济负担。" },
{ "type": "重疾", "summary": "客户目前为36岁-45岁处于单身贵族阶段无家庭负担但关注点为医疗健康。这意味着客户可能面临较高的重疾风险。根据统计数据中国新发恶性肿瘤的发病数每年都在增加且发病年龄呈下降趋势35-44岁组发病人数占全部恶性肿瘤发病人数的40%,且发病与工作压力、生活压力、环境污染、不良生活习惯等密切相关。考虑到客户目前的职业为赋闲大众,生活压力可能较大,因此重疾风险不容忽视。建议客户购买重疾险,以减轻因重疾带来的经济负担,保障自己能够得到及时和有效的治疗。" }
],
clientpreferenceVOListCopy: [],
index: 0,
ind: 0,
timer: null
},
mounted() {
this.typeWriter();
},
methods: {
typeWriter() {
const item = this.clientpreferenceVOList[this.ind];
let o = { type: item.type, summary: '' };
this.clientpreferenceVOListCopy.push(o);
console.log('this.clientpreferenceVOListCopy', this.clientpreferenceVOListCopy);
this.timer = setInterval(() => {
this.dealO();
}, 50);
},
dealO() {
console.log('this.index', this.index, this.clientpreferenceVOList[this.ind].summary.charAt(this.index));
if (this.index < this.clientpreferenceVOList[this.ind].summary.length) {
// 将当前字符添加到文本元素中
this.clientpreferenceVOListCopy[this.ind].summary += this.clientpreferenceVOList[this.ind].summary.charAt(this.index);
this.index++;
} else {
// 设置延迟后继续执行函数
clearInterval(this.timer);
this.timer = null;
this.index = 0;
this.ind++;
if (this.ind <= 2) {
this.typeWriter();
}
}
}
},
beforeDestroy() {
// 清除定时器
clearInterval(this.timer);
this.timer = null;
}
})
</script>
</body>
</html>

199
hevc-to-h264-player.js Normal file
View File

@@ -0,0 +1,199 @@
class HevcToH264Player {
constructor() {
// 关键修正:使用正确的核心库路径(注意文件后缀和目录)
this.config = {
ffmpegScriptUrl: "https://cdnjs.cloudflare.com/ajax/libs/ffmpeg/0.12.15/umd/ffmpeg.min.js",
// 核心库必须使用完整路径包含wasm文件的正确位置
ffmpegCorePath: "https://cdnjs.cloudflare.com/ajax/libs/ffmpeg-core/0.12.10/esm/ffmpeg-core.js",
};
// DOM元素
this.fileInput = document.getElementById("hevcFileInput");
this.statusEl = document.getElementById("conversionStatus");
this.progressBar = document.getElementById("progressBar");
this.videoContainer = document.getElementById("videoContainer");
// 状态变量
this.ffmpeg = null;
this.isLoaded = false;
this.coreLoaded = false;
// 初始化
this.bindEvents();
this.createVideoElement();
}
// 创建视频元素
createVideoElement() {
this.video = document.createElement("video");
this.video.controls = true;
this.video.style.width = "100%";
this.video.style.display = "none";
this.videoContainer.appendChild(this.video);
this.loadingDiv = document.createElement("div");
this.loadingDiv.style.cssText = `
position: absolute; top:0; left:0; width:100%; height:100%;
background:rgba(0,0,0,0.7); color:white; display:flex;
flex-direction:column; justify-content:center; align-items:center;
`;
this.loadingDiv.innerHTML = '<div class="spinner"></div><div id="loadingText">准备中...</div>';
this.videoContainer.appendChild(this.loadingDiv);
this.loadingText = this.loadingDiv.querySelector("#loadingText");
// 添加动画样式
const style = document.createElement("style");
style.textContent = `
.spinner { border:4px solid rgba(255,255,255,0.3); border-top:4px solid white;
border-radius:50%; width:40px; height:40px; animation:spin 1s linear infinite; }
@keyframes spin { 0% { transform:rotate(0deg); } 100% { transform:rotate(360deg); } }
`;
document.head.appendChild(style);
}
// 绑定事件
bindEvents() {
this.fileInput.addEventListener("change", (e) => {
if (e.target.files.length) {
this.convert(e.target.files[0]);
}
});
}
// 加载主库
async loadMainLibrary() {
return new Promise((resolve, reject) => {
// if (window.FFmpeg && window.FFmpeg.createFFmpeg) {
// resolve(window.FFmpeg);
// return;
// }
const script = document.createElement("script");
script.src = this.config.ffmpegScriptUrl;
script.type = "text/javascript";
script.onload = () => {
if (window.FFmpeg && window.FFmpeg.createFFmpeg) {
resolve(window.FFmpeg);
} else {
reject(new Error("主库加载但未找到createFFmpeg方法版本不兼容"));
}
};
script.onerror = () => reject(new Error("主库加载失败网络或URL错误"));
document.head.appendChild(script);
});
}
// 验证核心库是否加载(关键修正)
checkCoreLibraryLoaded() {
return new Promise((resolve) => {
const checkInterval = setInterval(() => {
// 检查核心库的WASM文件是否已加载
const wasmLoaded = document.querySelectorAll(`script[src*="ffmpeg-core.wasm"]`).length > 0;
if (wasmLoaded) {
clearInterval(checkInterval);
this.coreLoaded = true;
resolve();
}
}, 200);
// 核心库加载超时30秒
setTimeout(() => {
clearInterval(checkInterval);
resolve(); // 超时也继续让FFmpeg自己处理错误
}, 30000);
});
}
// 初始化FFmpeg
async initFFmpeg() {
if (this.ffmpeg && this.isLoaded) return;
this.updateStatus("加载主库...");
const FFmpeg = await this.loadMainLibrary();
console.log("FFmpeg", FFmpeg);
this.updateStatus("初始化FFmpeg实例...");
this.ffmpeg = FFmpeg.createFFmpeg({
log: true,
corePath: this.config.ffmpegCorePath,
});
this.updateStatus("加载核心库约20MB...");
// 开始加载核心库
const loadPromise = this.ffmpeg.load();
// 等待核心库加载迹象
// await this.checkCoreLibraryLoaded();
// 等待加载完成
await loadPromise;
this.isLoaded = true;
this.updateStatus("FFmpeg完全加载成功");
}
// 更新状态
updateStatus(text) {
if (this.statusEl) this.statusEl.textContent = text;
if (this.loadingText) this.loadingText.textContent = text;
}
// 转码主逻辑
async convert(file) {
this.updateStatus("准备转码...");
this.progressBar.style.width = "0%";
this.loadingDiv.style.display = "flex";
this.video.style.display = "none";
try {
await this.initFFmpeg();
// 写入文件
const inputName = "input.hevc";
const outputName = "output.mp4";
this.ffmpeg.FS("writeFile", inputName, new Uint8Array(await file.arrayBuffer()));
// 转码进度
this.ffmpeg.setProgress(({ ratio }) => {
const progress = Math.round(ratio * 100);
this.progressBar.style.width = `${progress}%`;
this.updateStatus(`转码中:${progress}%`);
});
// 执行转码
this.updateStatus("开始转码...");
await this.ffmpeg.run("-i", inputName, "-c:v", "libx264", "-crf", "23", "-preset", "medium", "-c:a", "aac", "-b:a", "128k", outputName);
// 播放结果
const data = this.ffmpeg.FS("readFile", outputName);
const url = URL.createObjectURL(new Blob([data.buffer], { type: "video/mp4" }));
this.video.src = url;
this.video.onloadedmetadata = () => {
this.video.style.display = "block";
this.loadingDiv.style.display = "none";
this.updateStatus("转码完成,正在播放");
this.video.play().catch(() => {});
};
} catch (error) {
// 核心库未加载的特殊处理
if (!this.coreLoaded) {
error.message = "核心库加载失败请检查网络或尝试更换CDN";
}
this.updateStatus(`转码失败:${error.message}`);
// console.error("错误详情:", error);
}
}
}
// 页面加载后初始化
document.addEventListener("DOMContentLoaded", () => {
// 确保DOM元素存在
if (["hevcFileInput", "conversionStatus", "progressBar", "videoContainer"].every((id) => document.getElementById(id))) {
window.hevcPlayer = new HevcToH264Player();
} else {
console.error("缺少必要的DOM元素");
}
});

View File

@@ -2,32 +2,32 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="referrer" content="no-referrer" />
<title>5222</title>
<link rel="stylesheet" href="./css/index.css" />
<title>你好,招生官专题 - 寄托天下</title>
<meta name="Description" content="【你好,招生官】是寄托天下将海外及各类合作办学招生项目直接介绍给寄托用户的品牌活动。 我们提供学生和招生官直接互动的平台,帮助学生更好的了解项目,做出选择。" />
<meta name="Keywords" content="美国留学,加拿大留学,香港留学,新加坡留学,英国留学,欧洲留学, 留学经验分享,DIY留学,留学申请,留学流程,留学费用,出国留学,留学论坛, 留学网站,留学考试,GRE,TOEFL,IBT,GMAT,IELTS,SAT,VISA,文书,签证" />
<link rel="stylesheet" href="https://app.gter.net/image/gter/admissionofficer/css/admissionOfficerV2.css" />
<link rel="stylesheet" href="{~forum/css/header-floor.css}" />
<style>
[v-cloak] {
display: none;
}
.svg1 {
width: 100%;
min-width: 1920px;
}
</style>
<script src="https://app.gter.net/bottom?tpl=header&menukey=admissionOfficer"></script>
</head>
<body>
<video class="vvideo" src=""></video>
<div id="admission-officer" class="admission-officer" v-cloak>
<div class="admission-head">
<div class="admission-head-box">
<img class="admission-head-logo" src="./img/admission-head-logo.png" />
<div class="admission-head-gray"></div>
<img class="admission-head-1" src="/img/admission-head-green.svg" />
<img class="admission-head-2" src="/img/admission-head-gray.svg" />
<!-- <div class="admission-head-gray"></div> -->
<!-- <img class="admission-head-1" src="{~admissionofficer/imgV2/1.svg}" /> -->
<img class="admission-head-1" src="https://app.gter.net/image/gter/admissionofficer/imgV2/2.svg" />
<!-- https://app.gter.net/image/gter/passport/index.html?islogin=1#/loginSuccess -->
<!-- <img class="admission-head-2" src="{~admissionofficer/imgV2/admission-head-gray.svg}" /> -->
</div>
</div>
<img class="svg1" style="position: relative; margin-top: -22px" src="./img/1.svg" />
<div class="admission-body">
<div class="interview-box flexflex" v-if="theme == 1">
<img class="bj" src="./img/interview-bj.svg" />
@@ -37,8 +37,6 @@
<img class="name" src="./img/interview-name.png" />
</div>
<div class="content flexcenter">
<!-- <video class="img" :controls="isPlaying ? true : false" src="https://cdnfhnfile.115cdn.net/6870d9500edfa26822026f57689fdaa03fc219f0/%E5%B2%AD%E5%8D%97%E5%A4%A7%E5%AD%A6%E4%BF%9D%E9%99%A9.mp4?t=1752491453&u=100031698&s=524288000&d=vip-3070654841-cg2ys1g7vy7k4dxg0-1-0&c=2&f=1&k=efc45551ccbd233122fe5dc2c53b5b53&us=5242880000&uc=10&v=1" preload="none" poster="https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-d_5otcn_qqsgFptxhXa6QWi2uePJ5Bg8WFLPIqoYV7MtdWWmQtw3_-kU8uRQ0NDI5" @pause="isPlaying = false"></video> -->
<!-- <video class="img" autoplay src="https://dl1-v6.aliyundrive.cloud/dC9ELbBd%2F988690082%2F6870d948ef3b30d87fa84bc3a32fd16370f37c2e%2F6870d948f17001272f7c4c17b11d759a137ca234?ap=b8c990e60b18446eb07f5dca30398e8a&callback=eyJjYWxsYmFja1VybCI6Imh0dHA6Ly9iajI5LmFwaS1ocC5hbGl5dW5wZHMuY29tL3YyL2ZpbGUvZG93bmxvYWRfY2FsbGJhY2siLCJjYWxsYmFja0JvZHkiOiJodHRwSGVhZGVyLnJhbmdlPSR7aHR0cEhlYWRlci5yYW5nZX1cdTAwMjZidWNrZXQ9JHtidWNrZXR9XHUwMDI2b2JqZWN0PSR7b2JqZWN0fVx1MDAyNmRvbWFpbl9pZD0ke3g6ZG9tYWluX2lkfVx1MDAyNnVzZXJfaWQ9JHt4OnVzZXJfaWR9XHUwMDI2ZHJpdmVfaWQ9JHt4OmRyaXZlX2lkfVx1MDAyNmZpbGVfaWQ9JHt4OmZpbGVfaWR9XHUwMDI2cGRzX3BhcmFtcz0ke3g6cGRzX3BhcmFtc31cdTAwMjZ2ZXJzaW9uPSR7eDp2ZXJzaW9ufSIsImNhbGxiYWNrQm9keVR5cGUiOiJhcHBsaWNhdGlvbi94LXd3dy1mb3JtLXVybGVuY29kZWQiLCJjYWxsYmFja1N0YWdlIjoiYmVmb3JlLWV4ZWN1dGUiLCJjYWxsYmFja0ZhaWx1cmVBY3Rpb24iOiJpZ25vcmUifQ%3D%3D&callback-var=eyJ4OmRvbWFpbl9pZCI6ImJqMjkiLCJ4OnVzZXJfaWQiOiI3MDg3MzAwYmU2ZDM0OGMwODliOTU4NDM1MDVlN2U3MiIsIng6ZHJpdmVfaWQiOiI5ODg2OTAwODIiLCJ4OmZpbGVfaWQiOiI2ODcwZGViOTI4MTdiODljNGFmOTQyM2E5NWY0ZGU1MmY2YjA0N2M1IiwieDpwZHNfcGFyYW1zIjoie1wiYXBcIjpcImI4Yzk5MGU2MGIxODQ0NmViMDdmNWRjYTMwMzk4ZThhXCJ9IiwieDp2ZXJzaW9uIjoidjMifQ%3D%3D&di=bj29&dr=988690082&f=6870deb92817b89c4af9423a95f4de52f6b047c5&op=d&pds-params=%7B%22ap%22%3A%22b8c990e60b18446eb07f5dca30398e8a%22%7D&response-content-disposition=attachment%3B%20filename%2A%3DUTF-8%27%27%25E5%25B2%25AD%25E5%258D%2597%25E5%25A4%25A7%25E5%25AD%25A6%25E4%25BF%259D%25E9%2599%25A9.mp4&security-token=CAISvgJ1q6Ft5B2yfSjIr5nDE%2FfhmY5U8Kbeb0DZi1A7WMdt2fOZrjz2IHhMf3NpBOkZvvQ1lGlU6%2Fcalq5rR4QAXlDfNVT2OWG%2BqlHPWZHInuDox55m4cTXNAr%2BIhr%2F29CoEIedZdjBe%2FCrRknZnytou9XTfimjWFrXWv%2Fgy%2BQQDLItUxK%2FcCBNCfpPOwJms7V6D3bKMuu3OROY6Qi5TmgQ41Uh1jgjtPzkkpfFtkGF1GeXkLFF%2B97DRbG%2FdNRpMZtFVNO44fd7bKKp0lQLs0ARrv4r1fMUqW2X543AUgFLhy2KKMPY99xpFgh9a7j0iCbSGyUu%2FhcRm5sw9%2Byfo34lVYnewzJcyhXOy4IClLcc%2BmqdsRIvJzWstJ7Gf9LWqChvSgk4TxhhcNFKSTQrInFCB0%2BcRObJl16iK%2BF7UPXtuMkagAFphP8r3Ae8eUgHGTFSUZ16y1LK9OcgxB5wF%2B1Y%2F2WkLq22JaopX124joWne0xEWtK%2BD3nQ57EtFJXAH5XFVSpL8ey6q7%2B0SYYKfnDvV4AlMij6c73j2p%2BFZfWoqOtRPih9DHBr8rG%2FGUthBwtO1PmFMMhnhUaPX0LH3mDT7kxj4yAA&u=7087300be6d348c089b95843505e7e72&x-oss-access-key-id=STS.NZvXMUtQuGd5mfhoPnTHA662L&x-oss-expires=1752561724&x-oss-signature=%2FVN5mf0vGLN3W4Sothr0YCBJ1gs1pWpKt2zZE0Vw9v8%3D&x-oss-signature-version=OSS2" @pause="isPlaying = false"></video> -->
<img class="img" :src="interviewData.video_cover" />
<img class="play-btn" src="./img/play-btn.svg" @click="getVideoUrl(interviewData.token)" :style="{ display: isPlaying ? 'none' : 'block' }" />
<div class="bottom" :style="{ transform: isPlaying ? 'translateY(100%)' : 'translateY(0)', opacity: isPlaying ? '0' : '1' }">
@@ -83,18 +81,18 @@
<img class="bj" src="./img/interview-bj.svg" />
<div class="left">
<div class="head flexacenter">
<img class="icon" src="./img/preach-icon.png" />
<img class="name" src="./img/preach-name.png" />
<img class="icon" src="./img/interview-icon.png" />
<img class="name" :src="preachList[preachIndex]?.istop ? './img/interview-name.png' : './img/preach-name.png'" />
</div>
<div class="box" @mouseenter="preachMouseEnter" @mouseleave="preachMouseLeave">
<div class="indicators flexcenter">
<div class="item" :class="{'pitch': index == preachIndex}" v-for="(item, index) in preachList.length" @click="preachIndex = index;preachI = 0"></div>
<div class="item" :class="{'pitch': index == preachIndex}" v-for="(item, index) in preachList.length" @click="cutPreach(index)"></div>
</div>
<div class="list">
<div class="item flexacenter" :class="[{'pitch': preachI == index},{'pitch-last': preachI - 1 == index}]" v-for="(item, index) in preachList[preachIndex]" @click="preachI = index">
<div class="list" ref="leftPreach">
<div class="item flexacenter" :class="[{'pitch': preachIndex == index},{'pitch-last': preachIndex - 1 == index}]" v-for="(item, index) in preachList" @click="cutPreach(index)" ref="leftPreachItem">
<div class="info flex1">
<div class="name">{{ item.title }}</div>
<div class="time">{{ item.lecture_time || '长期答疑' }}</div>
<div class="name one-line-display">{{ item.title }}</div>
<div class="time">{{ item.lecture_time || "长期答疑" }}</div>
</div>
<img class="icon" src="./img/arrows-full-circle-white.svg" />
</div>
@@ -104,9 +102,32 @@
<div class="right" @mouseenter="preachMouseEnter" @mouseleave="preachMouseLeave">
<img class="ok" src="./img/ok.png" />
<div class="img-box flexacenter" ref="rightImgBox">
<a class="item" v-for="(item, index) in preachList[preachIndex]" target="_blank" :href="item.link_url">
<img class="img" :src="item.image_url" />
</a>
<template v-for="(item, index) in preachList" :key="index">
<div v-if="item.istop" class="video-img flexacenter">
<img class="play-btn" @click="getVideoUrl(item.token)" src="./img/play-btn.svg" />
<img class="img" :src="item.video_cover" />
<div class="bottom" :style="{ transform: isPlaying ? 'translateY(100%)' : 'translateY(0)', opacity: isPlaying ? '0' : '1' }">
<div class="title">{{ item.title }}</div>
<div class="subtitle">{{ item.subtitle }}</div>
</div>
<div class="interview-info flexflex">
<img class="interview-side-bj" src="https://app.gter.net/image/gter/admissionofficer/imgV2/interview-side-bj.svg" />
<div class="interview-title">
<img class="interview-title-icon" src="https://app.gter.net/image/gter/admissionofficer/imgV2/interview-title-bj.svg" />
访谈人物
</div>
<div class="interview-name flexflex">
{{ item.admission_officer_name }}
<div class="interview-professional">{{ item.admission_officer_rank }}</div>
</div>
<div class="interview-subheading margin-b-10">{{ item.name }}</div>
<div class="interview-subheading">{{ item.admission_officer_position }}</div>
</div>
</div>
<a v-else class="item" target="_blank" :href="item.link_url">
<img class="img" :src="item.image_url" />
</a>
</template>
</div>
</div>
</div>
@@ -148,15 +169,13 @@
</div>
<div class="right">
<div class="info flexacenter">
<a :href="`https://schools.gter.net/details/${item.sid}`" target="_blank">
<img class="img" :src="item.logo" />
</a>
<img class="img" :src="item.logo" @click="goDetails(item.sid)" />
<div class="flexflex" style="flex-direction: column">
<a class="name" :href="`https://schools.gter.net/details/${item.sid}`" target="_blank">
{{ item.name }}
<div class="name flexacenter" :href="`https://schools.gter.net/details/${item.sid}`" target="_blank" @click="goDetails(item.sid)">
<div>{{ item.name }}</div>
<img class="arrows" src="./img/arrows-circle-black.svg" />
</a>
<a class="english" :href="`https://schools.gter.net/details/${item.sid}`" target="_blank">{{ item.enname }}</a>
</div>
<div class="english" :href="`https://schools.gter.net/details/${item.sid}`" target="_blank" @click="goDetails(item.sid)">{{ item.enname }}</div>
</div>
</div>
@@ -171,7 +190,7 @@
{{ it }}季
</div>
<div class="item more" :class="[{'unfold': item.state}, {'pitch': item.yListMore.includes(item.pitch)}]" v-if="item.yListMore.length > 0" @click="openSchoolYearState(index)">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="100px" height="28px" :style="{ fill: item.yListMore.includes(item.pitch) ? item.color : '' }">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="100px" height="28px" :style="{ fill: item.yListMore.includes(item.pitch) || item.state ? item.color : '' }">
<g transform="matrix(1 0 0 1 -910 -1138 )"><path d="M 910 1166 L 918.4 1138 L 1010 1138 L 1001.6 1166 L 910 1166 Z " fill-rule="nonzero" stroke="none" /></g>
</svg>
<!-- 更多 -->
@@ -181,7 +200,7 @@
</svg>
<div class="more-mask" @click.stop="closeSchoolYearState(index)"></div>
<div class="more-box" :style="{ backgroundColor: item.color }">
<div class="more-item" v-for="it in item.yListMore" @click.stop="selectSchoolYearState(index, it)">{{ it }}季1</div>
<div class="more-item" v-for="it in item.yListMore" @click.stop="selectSchoolYearState(index, it)">{{ it }}季</div>
</div>
</div>
</div>
@@ -205,7 +224,7 @@
<div class="icon flexcenter">
<img class="img" src="./img/time-icon.png" />
</div>
{{ it.date || '长期答疑'}}
{{ it.date || "长期答疑" }}
</div>
<a class="btn flexcenter" :style="{ 'background-color': item.color }" target="_blank" :href="it.url">
了解详情
@@ -229,18 +248,31 @@
<div class="name">{{ item.name }}</div>
<div class="english">{{ item.enname }}</div>
<div class="year flexacenter">
<div class="year-item" v-for="it in item.year.slice(0,4)">
<div class="year-item" v-for="it in item.year.length > 5 ? item.year.slice(0,4) : item.year" @click="openMoreSchool(item.sid,it)">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="80px" height="24px">
<g transform="matrix(1 0 0 1 -841 -2919 )"><path d="M 841 2943 L 848.2 2919 L 921 2919 L 913.8 2943 L 841 2943 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" /></g>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="841" y1="2920.97" x2="920.847764034253" y2="2920.97" id="LinearGradient1581">
<stop id="Stop1582" stop-color="#d6e5e5" offset="0" />
<stop id="Stop1583" stop-color="#f6f2ea" offset="1" />
</linearGradient>
</defs>
<g transform="matrix(1 0 0 1 -841 -2909 )"><path d="M 841 2933 L 848.2 2909 L 921 2909 L 913.8 2933 L 841 2933 Z " fill-rule="nonzero" fill="url(#LinearGradient1581)" stroke="none" /></g>
</svg>
{{ it }}季
</div>
<div class="year-item" v-if="item.year.length > 4">
<div class="year-item" v-if="item.year.length > 5">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="80px" height="24px">
<g transform="matrix(1 0 0 1 -841 -2919 )"><path d="M 841 2943 L 848.2 2919 L 921 2919 L 913.8 2943 L 841 2943 Z " fill-rule="nonzero" fill="#ffffff" stroke="none" /></g>
<defs>
<linearGradient gradientUnits="userSpaceOnUse" x1="841" y1="2920.97" x2="920.847764034253" y2="2920.97" id="LinearGradient1581">
<stop id="Stop1582" stop-color="#d6e5e5" offset="0" />
<stop id="Stop1583" stop-color="#f6f2ea" offset="1" />
</linearGradient>
</defs>
<g transform="matrix(1 0 0 1 -841 -2909 )">
<path d="M 841 2933 L 848.2 2909 L 921 2909 L 913.8 2933 L 841 2933 Z " fill-rule="nonzero" fill="url(#LinearGradient1581)" stroke="none" />
</g>
</svg>
更多
<img class="arrows" src="./img/arrows-triangle-black.svg" />
更多<img class="arrows" src="./img/arrows-triangle-black.svg" />
</div>
</div>
</div>
@@ -250,34 +282,31 @@
<img v-if="retrospectPage == 1" class="arrows gray" src="./img/arrows-thin-gray.svg" />
<img v-else class="arrows rotate180" @click="retrospectPage--" src="./img/arrows-thin-black.svg" />
<div class="item flexcenter" :class="{'pitch': item == retrospectPage}" v-for="item in retrospectPages" @click="retrospectPage = item">{{ item }}</div>
<img v-if="retrospectPage == retrospectPages" class="arrows gray rotate180" src="./img/arrows-thin-gray.svg" />
<img v-else class="arrows" @click="retrospectPage++" src="./img/arrows-thin-black.svg" />
<img v-if="retrospectPage == retrospectPages" class="arrows gray rotate180" src="{~admissionofficer/imgV2/arrows-thin-gray.svg}" />
<img v-else class="arrows" @click="retrospectPage++" src="{~admissionofficer/imgV2/arrows-thin-black.svg}" />
</div>
</div>
<div class="more-school-mask flexcenter" v-if="moreSchoolSid > 0" @click="closeMoreSchool">
<div class="more-school" @click.stop="">
<img class="close" @click.stop="closeMoreSchool" src="./img/cross.png" />
<img class="close" @click.stop="closeMoreSchool" src="{~admissionofficer/imgV2/cross.png}" />
<div class="head flexacenter">
<a :href="`https://schools.gter.net/details/${moreSchoolSid}`" target="_blank"><img class="img" :src="moreSchoolData.logo" /></a>
<a :href="moreSchoolUrl" target="_blank"><img class="img" :src="moreSchoolData.logo" /></a>
<div class="info">
<a class="name flexacenter" :href="`https://schools.gter.net/details/${moreSchoolSid}`" target="_blank">
{{ moreSchoolData.name }}
<img class="icon" src="./img/arrows-circle-black.svg" />
</a>
<a class="english" :href="`https://schools.gter.net/details/${moreSchoolSid}`" target="_blank">{{ moreSchoolData.enname }}</a>
<a class="name flexacenter" :href="moreSchoolUrl" target="_blank"> {{ moreSchoolData.name }}<img class="icon" src="{~admissionofficer/imgV2/arrows-circle-black.svg}" /> </a>
<a class="english" :href="moreSchoolUrl" target="_blank">{{ moreSchoolData.enname }}</a>
</div>
</div>
<div class="content">
<div class="year flexacenter">
<div class="item" :class="{'pitch': item == moreSchoolPitch}" v-for="(item,index) in moreSchoolYList" @click="moreSchoolPitch = item">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="100px" height="28px" fill="#3c7de9">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="100px" height="28px" :style="{ fill: moreSchoolPitch == item ? (moreSchoolData?.color || '#3c7de9') : '' }">
<g transform="matrix(1 0 0 1 -910 -1138 )"><path d="M 910 1166 L 918.4 1138 L 1010 1138 L 1001.6 1166 L 910 1166 Z " fill-rule="nonzero" stroke="none" /></g>
</svg>
{{ item }}季
</div>
<div class="item more" :class="[{'unfold': moreYearState},{'pitch': moreSchoolYMList.includes(moreSchoolPitch)}]" v-if="moreSchoolYMList.length > 0" @click="openMoreYearState">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="100px" height="28px" fill="#ffffff">
<svg class="svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns="http://www.w3.org/2000/svg" version="1.1" width="100px" height="28px" :style="{ fill: moreSchoolYMList.includes(moreSchoolPitch) || moreYearState ? (moreSchoolData?.color || '#3c7de9') : '' }">
<g transform="matrix(1 0 0 1 -910 -1138 )"><path d="M 910 1166 L 918.4 1138 L 1010 1138 L 1001.6 1166 L 910 1166 Z " fill-rule="nonzero" stroke="none" /></g>
</svg>
{{ moreSchoolYMList.includes(moreSchoolPitch) ? `${ moreSchoolPitch }季` : '更多' }}
@@ -285,7 +314,7 @@
<g transform="matrix(1 0 0 1 -1370 -1149 )"><path d="M 8.8330078125 0.164930555555556 C 8.9443359375 0.274884259259259 9 0.405092592592591 9 0.555555555555556 C 9 0.706018518518518 8.9443359375 0.836226851851851 8.8330078125 0.946180555555556 L 4.8955078125 4.83506944444444 C 4.7841796875 4.94502314814815 4.65234375 5 4.5 5 C 4.34765625 5 4.2158203125 4.94502314814815 4.1044921875 4.83506944444444 L 0.1669921875 0.946180555555556 C 0.0556640625 0.836226851851851 0 0.706018518518518 0 0.555555555555556 C 0 0.405092592592591 0.0556640625 0.274884259259259 0.1669921875 0.164930555555556 C 0.2783203125 0.0549768518518512 0.41015625 0 0.5625 0 L 8.4375 0 C 8.58984375 0 8.7216796875 0.0549768518518512 8.8330078125 0.164930555555556 Z " fill-rule="nonzero" stroke="none" transform="matrix(1 0 0 1 1370 1149 )" /></g>
</svg>
<div class="more-mask" @click.stop="moreYearState = false"></div>
<div class="more-box">
<div class="more-box" :style="{ backgroundColor: moreSchoolData.color || '#3c7de9' }">
<div class="more-item" v-for="it in moreSchoolYMList" @click.stop="moreSchoolPitch = it;moreYearState = false;">{{ it }}季</div>
</div>
</div>
@@ -294,20 +323,21 @@
<div class="item" v-for="item in moreSchoolList[moreSchoolPitch]">
<div class="name flexacenter">
<div class="icon flexcenter">
<img class="img" src="../img/course-icon.png" />
<img class="img" src="{~admissionofficer/imgV2/course-icon.png}" />
</div>
{{ item.title }}
<div v-html="item.title"></div>
<!-- {{ item.title }} -->
</div>
<div class="bottom flexacenter">
<div class="time flexacenter">
<div class="icon flexcenter">
<img class="img" src="../img/time-icon.png" />
<img class="img" src="{~admissionofficer/imgV2/time-icon.png}" />
</div>
{{ item.date || '长期答疑'}}
{{ item.date || "长期答疑" }}
</div>
<a class="btn flexcenter" :href="item.url" target="_blank">
了解详情
<img class="arrows" src="../img/arrows-circle-white.svg" />
<img class="arrows" src="{~admissionofficer/imgV2/arrows-circle-white.svg}" />
</a>
</div>
</div>
@@ -318,20 +348,27 @@
<div class="paly-box-mask flexcenter" v-if="palyState">
<div class="paly-box">
<img class="close" src="../img/cross-white.svg" @click="closePalyState" />
<div class="paly-video"></div>
<img class="close" src="admissionofficer/imgV2/cross-white.svg" @click="closePalyState" />
<div class="paly-video" id="playerContainer">
<!-- <video ref="videoPlayer" autoplay loop muted style="height: 100%; width: 100%"></video> -->
</div>
</div>
</div>
</div>
</div>
<script src="https://app.gter.net/bottom?tpl=footer,popupnotification"></script>
<!-- 引入了 vue3 js 创建 vue3 实例 -->
<script src="./js/vue.global.min.js"></script>
<script src="./js/artplayer.js"></script>
<script src="/js/vue.global.min.js"></script>
<script src="/js/artplayer.js"></script>
<script src="https://app.gter.net/image/gter/admissionofficer/js/hls.min.js"></script>
<script src="//unpkg.byted-static.com/xgplayer/2.31.6/browser/index.js" charset="utf-8"></script>
<script src="//unpkg.byted-static.com/xgplayer-hls/2.5.2/dist/index.min.js" charset="utf-8"></script>
<script>
// 创建Vue3实例
const { createApp, onMounted, ref, onUnmounted } = Vue;
const { createApp, onMounted, ref, onUnmounted, watch } = Vue;
const admissionApp = createApp({
setup() {
const isPlaying = ref(false);
@@ -339,21 +376,11 @@
const showLeftBtn = ref(true);
const showRightBtn = ref(true);
const togglePlay = (token) => {
console.log("token", token);
// const video = document.querySelector(".content video");
// if (isPlaying.value) video.pause();
// else video.play();
// isPlaying.value = !isPlaying.value;
};
const checkBtnVisibility = () => {
if (swiperRef.value) {
const { scrollLeft, scrollWidth, clientWidth } = swiperRef.value;
showLeftBtn.value = scrollLeft > 0;
showRightBtn.value = scrollLeft + clientWidth < scrollWidth;
}
if (!swiperRef.value) return;
const { scrollLeft, scrollWidth, clientWidth } = swiperRef.value;
showLeftBtn.value = scrollLeft > 0;
showRightBtn.value = scrollLeft + clientWidth < scrollWidth;
};
const scrollLeft = () => {
@@ -376,9 +403,13 @@
onMounted(() => {
init();
if (theme.value == 2) getBannerList();
getAdmissionLists();
getRetrospectList();
const header = document.querySelector("header.page-header");
header.style.setProperty("background-color", "#3c7de9", "important");
header.style.position = "relative";
header.style.zIndex = 2;
});
let interviewData = ref({}); // 模式一 的第一个访谈
@@ -387,18 +418,54 @@
const init = () => {
fetchData("/v1/admissionsOfficer/interview").then((res) => {
if (res.code != 200) return;
let data = res.data || [];
data.forEach((element) => (element.focus_of_this_issue = element.focus_of_this_issue.split(/\r?\n/)));
if (theme.value == 1) {
let target = null;
const topItems = data.filter((item) => item.istop === 1);
if (topItems.length > 0) target = topItems[Math.floor(Math.random() * topItems.length)];
data = data.filter((item) => item.id !== target.id);
interviewData.value = target;
}
// for (let i = 0; i < 5; i++) {
// data.push({
// title: "岭南大学保险",
// subtitle: "香港商学院硕士都去卖保险了",
// video_cover: "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-c-Z0kdXrqqsgFptxhcq_cQnrlIKYkXFcXBq_D-81qNDQyOQ~~",
// sid: 350 + i,
// name: "岭南大学",
// enname: "Lingnan University",
// admission_officer_name: "施林佟",
// admission_officer_rank: "副教授",
// admission_officer_position: "数据科学学院助理院长",
// focus_of_this_issue: "寄托建立了具有公信力的品牌评价系统\n致力打造一个华人区最专业的留学交流平台\n成为一个寄托或关于梦想的垂直门户网站",
// istop: i < 1 ? 1 : 0,
// token: "D6zdTJ2cMaKd0g6nFqsa1JR5scupqIIg5Wuab33kATaY-myKGtBtHiygo4F5z8iY_Le6VEcmblBwOvlbiqvh1pyXKaET8hp8OsXbY1fFQwRKjq42OWQ3" + i,
// });
// }
// const svg = ["https://app.gter.net/image/gter/admissionofficer/imgV2/md5__034ba3bbb7a45df24b94bf3c9db05a1a.svg", "https://app.gter.net/image/gter/admissionofficer/imgV2/md5__71e35a50f46c4eba0d18cd6348c7311d.svg", "https://app.gter.net/image/gter/admissionofficer/imgV2/md5__7b0f31351c4f716f8d1c2282e889e986.svg", "https://app.gter.net/image/gter/admissionofficer/imgV2/md5__81e38e5a3645073d75888475689f68c8.svg"];
// // 将 svg 赋值给 图片
// data.forEach((element, index) => {
// element.video_cover = svg[index % svg.length];
// });
// data.forEach((element) => (element.focus_of_this_issue = element.focus_of_this_issue.split(/\r?\n/)));
// if (theme.value == 1) {
// let target = null;
// const topItems = data.filter((item) => item.istop === 1);
// if (topItems.length > 0) target = topItems[Math.floor(Math.random() * topItems.length)];
// data = data.filter((item) => item.token !== target.token);
// interviewData.value = target;
// }
data.forEach((element) => videoToken.push(element.token));
// 将 istop = 1 的拿出来
const topItems = data.filter((item) => item.istop === 1);
data = data.filter((item) => item.istop !== 1);
preachList.value = topItems;
data = data.filter((item) => item.istop !== 1);
interviewList.value = data || [];
getBannerList();
setTimeout(() => {
if (!swiperRef.value) return;
swiperRef.value.addEventListener("scroll", checkBtnVisibility);
@@ -408,26 +475,209 @@
};
let palyState = ref(false); // 播放弹窗状态
watch(palyState, (newValue) => {
if (newValue) document.body.style.overflow = "hidden";
else document.body.style.overflow = "unset";
});
let art = null; // 播放器实例
let videoPlayer = ref(null);
let hls = ref(null);
let videoToken = [];
const getVideoUrl = (token) => {
fetchData(`/v1/admissionsOfficer/videoUrl?token=${token}`).then((res) => {
console.log("res", res);
// https://api.gter.net/v1/video/slice?token=QTwxtvbQlEvCrLRhmDc8SwmHy2VUsrlWZ0b8y1H9aFqDBWPTHl6mXG8N92Irk7pfXw3k8QHfAqTnCFUwVWQ0Yzk~
fetchData(`/v1/video/task?token=${token}`).then((res) => {
if (!res.data.url) {
creationAlertBox("error", "视频不存在");
return;
}
palyState.value = true;
setTimeout(() => {
art = new Artplayer({
container: ".paly-box-mask .paly-box .paly-video",
url: res.data.url,
autoplay: true,
const m3u8Url = res.data.url;
// 初始化播放器,集成 HLS 插件
const player = new HlsPlayer({
id: "playerContainer", // 容器 DOM ID
url: "http://183.6.121.121:8000/v/media/69b1c500a22ff943c7091d2cfcfc246b/preset.m3u8", // HLS 视频地址m3u8
// plugins: [XGPlayerHLS], // 注册 HLS 插件
width: 800, // 宽度
height: 450, // 高度
autoplay: true, // 是否自动播放(受浏览器政策限制)
controls: true, // 是否显示控制栏
poster: res.data.preview_image, // 封面图
});
}, 500);
// let player = new HlsPlayer({
// id: "mse",
// url: "//sf1-cdn-tos.huoshanstatic.com/obj/media-fe/xgplayer_doc_video/hls/xgplayer-demo.m3u8",
// autoplay: true,
// playsinline: true,
// height: window.innerHeight,
// width: window.innerWidth,
// });
// 事件监听
player.on("play", () => {
console.log("视频开始播放");
});
player.on("error", (err) => {
console.error("播放错误:", err);
});
return;
if (!art) {
console.log("res.data.url", res.data.url);
art = new Artplayer({
container: ".paly-box-mask .paly-box .paly-video",
url: res.data.url,
autoplay: true,
fullscreen: true,
poster: res.data.preview_image,
type: res.data.type,
muted: true, // 建议开启静音自动播放,减少被浏览器拦截的概率
// hevc: isHEVCSupported,
// events: {
// error: function (err) {
// console.error("视频播放错误:", err);
// // 如果是编码不支持的错误,可以给出提示
// if (err.code === 4) {
// alert("您的浏览器可能不支持 H.265 编码的视频,请尝试使用 Safari 浏览器或更新您的浏览器");
// } else {
// alert("视频加载失败,请稍后重试");
// }
// closePalyState();
// },
// },
customType: {
m3u8: playM3u8,
},
});
} else {
art.url = res.data.url;
art.poster = res.data.preview_image;
art.play();
}
// 监听播放完成事件
art.on("video:ended", () => {
const index = videoToken.indexOf(token);
if (index + 1 == videoToken.length) getVideoUrl(videoToken[0]);
else getVideoUrl(videoToken[index + 1]);
});
art.on("error", (error, reconnectTime) => {
console.info(error, reconnectTime);
});
art.on("ready", () => {
// console.info(art.hls);
});
}, 50);
});
};
const checkHEVCSupport = () => {
const video = document.querySelector(".vvideo");
return video.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"') !== "";
};
const playM3u8 = (video, url, art) => {
if (Hls.isSupported()) {
if (art.hls) art.hls.destroy();
const hls = new Hls();
hls.loadSource(url);
hls.attachMedia(video);
art.hls = hls;
art.on("destroy", () => hls.destroy());
} else if (video.canPlayType("application/vnd.apple.mpegurl")) {
video.src = url;
} else {
art.notice.show = "Unsupported playback format: m3u8";
}
};
// const playM3u8 = (video, url, art) => {
// if (Hls.isSupported()) {
// if (art.hls) art.hls.destroy();
// const hls = new Hls({
// // 添加调试日志配置
// debug: true,
// // 增加超时设置
// timeout: 10000,
// maxBufferLength: 30,
// maxMaxBufferLength: 600,
// });
// // 清除超时计时器
// const clearLoadTimeout = () => {
// if (loadTimeout) {
// clearTimeout(loadTimeout);
// loadTimeout = null;
// }
// };
// // 设置15秒加载超时
// loadTimeout = setTimeout(() => {
// console.error("HLS加载超时");
// art.notice.show = "视频加载超时,请刷新重试";
// hls.destroy();
// }, 15000);
// // 监听HLS各阶段事件
// hls.on("manifestParsed", () => {
// clearLoadTimeout();
// console.log("HLS: 清单解析完成");
// });
// hls.on("levelLoaded", (event, data) => {
// console.log(`HLS: 级别加载完成 [${data.level}]`);
// console.log(`分辨率: ${data.details.width}x${data.details.height}, 带宽: ${data.details.bitrate}bps`);
// });
// hls.on("fragmentLoaded", (event, data) => {
// console.log(`HLS: 分片加载完成 [${data.level}] ${data.frag.url}`);
// console.log(`已加载大小: ${Math.round(data.stats.loaded / 1024 / 1024)}MB`);
// });
// hls.on("error", (event, data) => {
// clearLoadTimeout();
// console.error("HLS错误:", data);
// // ... 保留原错误处理逻辑 ...
// });
// // 视频元素事件监听
// video.addEventListener("loadedmetadata", () => {
// console.log("视频元数据加载完成");
// });
// video.addEventListener("canplay", () => {
// clearLoadTimeout();
// console.log("视频可播放");
// });
// hls.loadSource(url);
// hls.attachMedia(video);
// art.hls = hls;
// art.on("destroy", () => {
// clearLoadTimeout();
// hls.destroy();
// });
// } else if (video.canPlayType("application/vnd.apple.mpegurl")) {
// video.src = url;
// } else {
// art.notice.show = "Unsupported playback format: m3u8";
// }
// };
const closePalyState = () => {
art.pause();
art.destroy();
art?.pause();
art?.destroy();
art = null;
palyState.value = false;
};
@@ -449,31 +699,29 @@
});
item["pitch"] = year[0];
item["yList"] = year.slice(0, 4);
item["yListMore"] = year.slice(4);
if (year.length > 5) {
item["yList"] = year.slice(0, 4);
item["yListMore"] = year.slice(4);
} else {
item["yList"] = year;
item["yListMore"] = [];
}
item["list"] = obj;
});
admissionList.value = target;
});
};
// 切换招生官 院校 的 年份
const cutSchoolYear = (index, year) => {
admissionList.value[index]["pitch"] = year;
admissionList.value[index]["isPitchMore"] = false;
};
const cutSchoolYear = (index, year) => (admissionList.value[index]["pitch"] = year);
// 点击 招生官 年份 更多弹窗
const openSchoolYearState = (index) => {
admissionList.value[index]["state"] = true;
};
const openSchoolYearState = (index) => (admissionList.value[index]["state"] = true);
// 选择 招生官 年份 更多弹窗
const selectSchoolYearState = (index, year) => {
admissionList.value[index]["state"] = false;
admissionList.value[index]["pitch"] = year;
admissionList.value[index]["pitch"] = true;
};
// 关闭 招生官 年份 更多弹窗
@@ -491,8 +739,15 @@
if (res.code != 200) return;
const data = res.data || {};
let target = data.data || [];
target.forEach((item) => (item.year = item.year.sort((a, b) => b - a)));
target = target.sort((a, b) => {
const latestA = Math.max(...a.year);
const latestB = Math.max(...b.year);
return latestB - latestA;
});
const count = data.count || 0;
const pages = Math.ceil(count / retrospectInterval.value);
@@ -501,19 +756,20 @@
retrospectList.value = target;
});
};
let moreSchoolUrl = ref(0); // 选中 学校 url
let moreSchoolSid = ref(0); // 选中 学校 sid
let moreSchoolData = ref({}); // 学校信息
let moreSchoolList = ref([]); // 年份下 列表 数据
let moreSchoolPitch = ref(0); // 选中年份
let moreSchoolYList = ref([]); // 年份 列表
let moreSchoolYMList = ref([]); // 年份 更多
const openMoreSchool = (sid) => {
const openMoreSchool = (sid, year) => {
fetchData(`/v1/admissionsOfficer/articles?sid=${sid}`).then((res) => {
if (res.code != 200) return;
const data = res.data || {};
moreSchoolData.value = data.school;
let target = data.data || [];
console.log("target", target);
let obj = {};
let yearList = [];
target.forEach((element) => {
@@ -521,9 +777,19 @@
yearList.push(element.year);
});
moreSchoolYList.value = yearList.slice(0, 4);
moreSchoolYMList.value = yearList.slice(4);
moreSchoolPitch.value = yearList[0];
const isprogram = data.school.isprogram || false;
let url = `https://schools.gter.net/details/${sid}`;
if (isprogram) url = `https://program.gter.net/college/${sid}`;
moreSchoolUrl.value = url;
if (yearList.length > 5) {
moreSchoolYList.value = yearList.slice(0, 4);
moreSchoolYMList.value = yearList.slice(4);
} else {
moreSchoolYList.value = yearList;
moreSchoolYMList.value = [];
}
moreSchoolPitch.value = year || yearList[0];
moreSchoolList.value = obj;
moreSchoolSid.value = sid;
document.body.style.overflow = "hidden";
@@ -532,9 +798,7 @@
let moreYearState = ref(false);
const openMoreYearState = () => {
moreYearState.value = true;
};
const openMoreYearState = () => (moreYearState.value = true);
const closeMoreSchool = () => {
moreSchoolSid.value = 0;
@@ -556,33 +820,27 @@
fetchData(`/v1/admissionsOfficer/banner`).then((res) => {
if (res.code != 200) return;
for (let i = 0; i < 25; i++) {
res.data.push({
id: 1,
title: "香港中文大学 | 美国西北大学双硕士学位课程",
lecture_time: "2025-07-12 00:00:00",
image_url: "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-c-Z0rfHjqqsgFptxhT66SWgrlI64uMxcfWaHf9cJWpdxZDnzZ5RLrizQ0Mjk~",
image_id: 977797,
sort: 1,
link_url: "https://www.baidu.com",
status: 1,
created_at: "2025-07-11 16:59:10",
updated_at: "2025-07-11 17:09:33",
});
}
// for (let i = 0; i < 8; i++) {
// let obj = {
// id: 1,
// title: "昆山杜克大学 | 五大硕士项目",
// lecture_time: "2025.7.29 19:00",
// image_url: "https://oss.x-php.com/Zvt57TuJSUvkyhw-xG7Y2l-c-Z0rfHjqqsgFptxhT66SWgrlI64uMxcfWaHf9cJWpdxZDnzZ5RLrizQ0Mjk~",
// image_id: 977797,
// sort: 1,
// link_url: "https://bbs.gter.net/thread-2626517-1-1.html",
// status: 1,
// created_at: "2025-07-11 16:59:10",
// updated_at: "2025-07-11 17:09:33",
// };
// res.data.push(obj);
// }
const data = res.data;
data.forEach((element, index) => (element["id"] = index + 1));
data.forEach((element) => (element["lecture_time"] = timeformat(element["lecture_time"])));
let target = [];
for (let i = 0; i < data.length; i += preachInterval) {
target.push(data.slice(i, i + preachInterval));
}
console.log("target", target);
preachList.value = target;
preachList.value = preachList.value.concat(data);
openPreachSwiper();
});
@@ -590,6 +848,7 @@
const timeformat = (time) => {
time = time.replaceAll("-", "/"); // 修改格式
time = time.replaceAll(".", "/"); // 修改格式
let result = "";
var datetime = new Date(time);
var Nyear = datetime.getFullYear();
@@ -607,38 +866,56 @@
// 宣讲会 轮播图 的 定时器
const openPreachSwiper = () => {
clearTimeout(preachTimer);
preachTimer = setTimeout(() => {
preachI.value += 1;
if (preachI.value >= preachList.value[preachIndex.value].length) {
preachIndex.value += 1;
preachI.value = 0;
}
if (preachIndex.value >= preachList.value.length) {
preachIndex.value = 0;
preachI.value = 0;
}
preachIndex.value += 1;
if (preachIndex.value >= preachList.value.length) preachIndex.value = 0;
openPreachSwiper();
setTimeout(() => {
rightImgBox.value.scrollTo({
left: 690 * preachI.value,
behavior: "smooth",
});
}, 100);
headRolling();
}, 2000);
};
// 鼠标 移入 轮播图
const preachMouseEnter = () => {
clearTimeout(preachTimer);
};
const preachMouseEnter = () => clearTimeout(preachTimer);
// 鼠标 离开 轮播图
const preachMouseLeave = () => {
openPreachSwiper();
const preachMouseLeave = () => openPreachSwiper();
let leftPreach = ref(null);
let leftPreachItem = ref(null);
const cutPreach = (index) => {
preachIndex.value = index;
headRolling();
};
// 顶部 轮播图列表 和 右侧 轮播图 同步滚动
const headRolling = () => {
setTimeout(() => {
const index = preachIndex.value;
const container = leftPreach.value;
const maxScrollTop = container.scrollHeight - container.clientHeight;
// const top = leftPreachItem.value[index].getBoundingClientRect().top;
const listRect = container.getBoundingClientRect();
const itemRect = leftPreachItem.value[index].getBoundingClientRect();
// 计算需要滚动的距离(使目标元素居中偏下)
// const top = itemRect.top - listRect.top + container.scrollTop - container.clientHeight / 2 + itemRect.height / 2;
const top = itemRect.top - listRect.top + container.scrollTop - container.clientHeight / 2;
// 滚动的距离不能超过最大滚动距离
const finalScrollTop = Math.max(0, Math.min(top, maxScrollTop));
leftPreach.value.scrollTo({
top: finalScrollTop,
behavior: "smooth",
});
rightImgBox.value.scrollTo({
left: 690 * index,
behavior: "smooth",
});
}, 100);
};
const fetchData = (url, data) => {
@@ -671,8 +948,18 @@
});
};
let dynamicColor = ref("");
return { closePalyState, getVideoUrl, preachMouseEnter, preachMouseLeave, rightImgBox, palyState, theme, interviewList, closeSchoolYearState, selectSchoolYearState, openSchoolYearState, cutSchoolYear, admissionList, moreSchoolYMList, openMoreYearState, moreYearState, moreSchoolPitch, moreSchoolYList, dynamicColor, moreSchoolList, moreSchoolData, moreSchoolSid, closeMoreSchool, openMoreSchool, retrospectInterval, retrospectPage, retrospectPages, retrospectList, retrospectCount, retrospectList, interviewData, preachList, preachIndex, preachI, isPlaying, togglePlay, scrollLeft, scrollRight, swiperRef, showLeftBtn, showRightBtn };
const goDetails = (sid) => {
fetchData(`/v1/admissionsOfficer/articles?sid=${sid}`).then((res) => {
if (res.code != 200) return;
const data = res.data || {};
const isprogram = data.school.isprogram || false;
let url = `https://schools.gter.net/details/${sid}`;
if (isprogram) url = `https://program.gter.net/college/${sid}`;
window.open(url, "_blank");
});
};
return { videoPlayer, leftPreach, leftPreachItem, cutPreach, moreSchoolUrl, goDetails, closePalyState, getVideoUrl, preachMouseEnter, preachMouseLeave, rightImgBox, palyState, theme, interviewList, closeSchoolYearState, selectSchoolYearState, openSchoolYearState, cutSchoolYear, admissionList, moreSchoolYMList, openMoreYearState, moreYearState, moreSchoolPitch, moreSchoolYList, moreSchoolList, moreSchoolData, moreSchoolSid, closeMoreSchool, openMoreSchool, retrospectInterval, retrospectPage, retrospectPages, retrospectList, retrospectCount, retrospectList, interviewData, preachList, preachIndex, preachI, isPlaying, scrollLeft, scrollRight, swiperRef, showLeftBtn, showRightBtn };
},
});
// 挂载到页面中的#app元素