初始化多多畅职企业内推平台项目
功能特性: - 3D地球动画与中国地图可视化 - 省份/城市/企业搜索功能 - 308家企业数据展示 - 响应式设计(PC端和移动端) - 企业详情页面与业务板块展示 - 官网新闻轮播图 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
124
js/config.js
Normal file
124
js/config.js
Normal file
@@ -0,0 +1,124 @@
|
||||
/* ===================================
|
||||
全局配置常量
|
||||
=================================== */
|
||||
|
||||
import { activeCities, activeProvinces } from './data.js';
|
||||
|
||||
export const CONFIG = {
|
||||
// 地图配置
|
||||
map: {
|
||||
geoJsonUrls: {
|
||||
china: 'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json',
|
||||
// 更多省份可以在这里添加
|
||||
jiangsu: 'https://geo.datav.aliyun.com/areas_v3/bound/320000_full.json',
|
||||
hebei: 'https://geo.datav.aliyun.com/areas_v3/bound/130000_full.json',
|
||||
anhui: 'https://geo.datav.aliyun.com/areas_v3/bound/340000_full.json',
|
||||
hubei: 'https://geo.datav.aliyun.com/areas_v3/bound/420000_full.json',
|
||||
hunan: 'https://geo.datav.aliyun.com/areas_v3/bound/430000_full.json'
|
||||
},
|
||||
activeProvinces: activeProvinces, // 从data.js动态导入
|
||||
activeCities: activeCities, // 从data.js动态导入
|
||||
defaultZoom: 1.2
|
||||
},
|
||||
|
||||
// 3D场景配置
|
||||
scene: {
|
||||
camera: {
|
||||
fov: 60,
|
||||
near: 0.1,
|
||||
far: 2000,
|
||||
initialZ: 100, // 开场动画起点
|
||||
defaultZ: 16 // 默认位置
|
||||
},
|
||||
earth: {
|
||||
radius: 5,
|
||||
segments: 64,
|
||||
cloudRadius: 5.08,
|
||||
atmosphereRadius: 5.35
|
||||
},
|
||||
stars: {
|
||||
count: 10000,
|
||||
size: 1.4,
|
||||
colors: [
|
||||
0xffffff, // 纯白
|
||||
0xaaddff, // 蓝白
|
||||
0xffddaa, // 金黄
|
||||
0xddaaff, // 淡紫
|
||||
0xffffff, // 增加白色权重
|
||||
0xffffff
|
||||
]
|
||||
},
|
||||
fog: {
|
||||
color: 0x020205,
|
||||
density: 0.015
|
||||
},
|
||||
lights: {
|
||||
ambient: 0x050510,
|
||||
sun: {
|
||||
color: 0xffffff,
|
||||
intensity: 1.5,
|
||||
position: [30, 10, 30]
|
||||
},
|
||||
rim: {
|
||||
color: 0x0088ff,
|
||||
intensity: 3,
|
||||
position: [-30, 20, -5]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 动画时长配置
|
||||
animation: {
|
||||
intro: {
|
||||
cameraDuration: 5, // 摄像机推进时长(秒)
|
||||
titleDelay: 1.5, // 标题出现延迟
|
||||
subtitleDelay: 0.8, // 副标题延迟
|
||||
hintDelay: 0.5 // 提示延迟
|
||||
},
|
||||
transition: {
|
||||
earthRotation: 1.2, // 地球旋转时长
|
||||
cameraZoom: 1.5, // 镜头冲刺时长
|
||||
fogFade: 0.8 // 云雾出现时长
|
||||
},
|
||||
ui: {
|
||||
fadeDuration: 0.5, // 界面切换淡入淡出
|
||||
cardStagger: 0.1, // 卡片依次出现间隔
|
||||
cardDelay: 0.3 // 卡片整体延迟
|
||||
}
|
||||
},
|
||||
|
||||
// 转场特效配置
|
||||
warp: {
|
||||
targetRotationY: 2.83, // 让中国朝向相机(东经105度)
|
||||
targetRotationX: 0.7, // 轻微向下倾斜
|
||||
finalCameraZ: 4.5, // 冲刺终点
|
||||
shakeThreshold: {
|
||||
min: 5,
|
||||
max: 8
|
||||
},
|
||||
shakeIntensity: 0.03
|
||||
},
|
||||
|
||||
// 纹理资源URL(使用腾讯云COS,国内访问稳定)
|
||||
textures: {
|
||||
earthMap: 'https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/duoduo_guanwang/earth_1.jpg',
|
||||
earthSpecular: 'https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/duoduo_guanwang/earth_2.jpg',
|
||||
earthNormal: 'https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/duoduo_guanwang/earth_3.jpg',
|
||||
earthClouds: 'https://ddcz-1315997005.cos.ap-nanjing.myqcloud.com/static/img/duoduo_guanwang/earth_4.png'
|
||||
},
|
||||
|
||||
// ECharts加载配置
|
||||
echarts: {
|
||||
loadingText: '正在加载全息数据...',
|
||||
loadingColor: '#38bdf8',
|
||||
loadingTextColor: '#fff',
|
||||
loadingMaskColor: 'rgba(0,0,0,0)'
|
||||
},
|
||||
|
||||
// 移动端适配
|
||||
mobile: {
|
||||
breakpoint: 768,
|
||||
touchSensitivity: 1.5,
|
||||
gyroscopeSensitivity: 0.5
|
||||
}
|
||||
};
|
||||
24454
js/data.js
Normal file
24454
js/data.js
Normal file
File diff suppressed because it is too large
Load Diff
285
js/main.js
Normal file
285
js/main.js
Normal file
@@ -0,0 +1,285 @@
|
||||
/* ===================================
|
||||
应用主入口 - 模块编排与路由控制
|
||||
=================================== */
|
||||
|
||||
import { SceneManager } from './scene/SceneManager.js';
|
||||
import { MapInterface } from './ui/MapInterface.js';
|
||||
import { ListInterface } from './ui/ListInterface.js';
|
||||
import { DetailInterface } from './ui/DetailInterface.js';
|
||||
import { SearchController } from './ui/SearchController.js';
|
||||
import { UIUtils } from './ui/UIUtils.js';
|
||||
import { initWebsite, switchPage } from './website.js';
|
||||
|
||||
class App {
|
||||
constructor() {
|
||||
// 官网 DOM 引用
|
||||
this.websiteContainer = document.getElementById('website-container');
|
||||
this.appContainer = document.getElementById('app-container');
|
||||
this.navbar = document.getElementById('navbar');
|
||||
|
||||
// 3D平台 DOM 引用
|
||||
this.canvasContainer = document.getElementById('canvas-container');
|
||||
this.mapInterface = document.getElementById('map-interface');
|
||||
this.listInterface = document.getElementById('list-interface');
|
||||
this.detailInterface = document.getElementById('detail-interface');
|
||||
this.mapLogoArea = document.getElementById('map-logo-area');
|
||||
|
||||
// UI 元素
|
||||
this.uiLayer = document.getElementById('ui-layer');
|
||||
this.hint = document.querySelector('.instruction-hint');
|
||||
this.speedLines = document.getElementById('speed-lines');
|
||||
this.cloudFog = document.getElementById('cloud-fog');
|
||||
|
||||
// 模块实例
|
||||
this.sceneManager = null;
|
||||
this.mapController = null;
|
||||
this.listController = null;
|
||||
this.detailController = null;
|
||||
|
||||
// 2D地图初始化标志,防止重复初始化导致省级地图被覆盖
|
||||
this.is2DInitialized = false;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化应用
|
||||
async init() {
|
||||
// 1. 优先初始化官网(立即执行,等待CSV加载)
|
||||
await initWebsite(() => this.switchToApp());
|
||||
|
||||
// 2. 初始化 3D 平台的控制器(但不启动场景)
|
||||
this.mapController = new MapInterface(
|
||||
this.mapInterface,
|
||||
(cityName) => this.showList(cityName)
|
||||
);
|
||||
|
||||
this.listController = new ListInterface(
|
||||
this.listInterface,
|
||||
(company) => this.showDetail(company),
|
||||
() => this.backToMap()
|
||||
);
|
||||
|
||||
this.detailController = new DetailInterface(
|
||||
this.detailInterface,
|
||||
() => this.backToList()
|
||||
);
|
||||
|
||||
// 初始化搜索控制器(PC端)
|
||||
const searchInput = document.getElementById('search-input');
|
||||
if (searchInput) {
|
||||
this.searchController = new SearchController(searchInput, {
|
||||
onSelectProvince: (provinceName) => {
|
||||
this.mapController.init('province', provinceName);
|
||||
},
|
||||
onSelectCity: (cityName) => {
|
||||
this.showList(cityName);
|
||||
},
|
||||
onSelectCompany: (company) => {
|
||||
// 设置当前城市为企业所属城市,以便返回时显示正确的列表
|
||||
this.listController.currentCity = company.city;
|
||||
this.showDetail(company);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化搜索控制器(移动端)
|
||||
const searchInputMobile = document.getElementById('search-input-mobile');
|
||||
if (searchInputMobile) {
|
||||
this.searchControllerMobile = new SearchController(searchInputMobile, {
|
||||
onSelectProvince: (provinceName) => {
|
||||
this.mapController.init('province', provinceName);
|
||||
},
|
||||
onSelectCity: (cityName) => {
|
||||
this.showList(cityName);
|
||||
},
|
||||
onSelectCompany: (company) => {
|
||||
// 设置当前城市为企业所属城市,以便返回时显示正确的列表
|
||||
this.listController.currentCity = company.city;
|
||||
this.showDetail(company);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. 绑定全局函数和事件
|
||||
this.bindGlobalFunctions();
|
||||
|
||||
// 4. 默认显示官网首页
|
||||
this.showWebsite();
|
||||
}
|
||||
|
||||
// 显示官网(确保官网可见,3D隐藏)
|
||||
showWebsite() {
|
||||
this.websiteContainer.classList.remove('hidden');
|
||||
this.appContainer.classList.add('hidden');
|
||||
this.navbar.classList.remove('-translate-y-full');
|
||||
}
|
||||
|
||||
// 切换到 3D 内推平台
|
||||
switchToApp() {
|
||||
// 隐藏官网
|
||||
this.websiteContainer.classList.add('hidden');
|
||||
|
||||
// 隐藏顶部导航栏(全屏体验)
|
||||
this.navbar.classList.add('-translate-y-full');
|
||||
|
||||
// 显示 3D 容器
|
||||
this.appContainer.classList.remove('hidden');
|
||||
|
||||
// 每次都重新初始化 3D 场景
|
||||
this.init3DScene();
|
||||
}
|
||||
|
||||
// 初始化 3D 场景(每次进入都重新加载)
|
||||
init3DScene() {
|
||||
// 如果有旧场景,先清理
|
||||
if (this.sceneManager) {
|
||||
this.sceneManager.dispose();
|
||||
this.sceneManager = null;
|
||||
}
|
||||
|
||||
// 重置UI状态
|
||||
this.canvasContainer.style.display = 'block';
|
||||
this.speedLines.style.display = 'block';
|
||||
this.cloudFog.style.display = 'none'; // 云雾初始隐藏,转场时才显示
|
||||
this.cloudFog.style.opacity = '0';
|
||||
this.mapInterface.style.display = 'none';
|
||||
|
||||
// 创建场景管理器
|
||||
this.sceneManager = new SceneManager(
|
||||
this.canvasContainer,
|
||||
() => this.onTransitionComplete()
|
||||
);
|
||||
|
||||
// 初始化转场系统
|
||||
this.sceneManager.initTransition({
|
||||
uiLayer: this.uiLayer,
|
||||
hint: this.hint,
|
||||
speedLines: this.speedLines,
|
||||
cloudFog: this.cloudFog
|
||||
});
|
||||
|
||||
// 启动开场动画
|
||||
this.sceneManager.startIntroSequence({
|
||||
uiLayer: this.uiLayer,
|
||||
hint: this.hint
|
||||
});
|
||||
}
|
||||
|
||||
// 从 3D 平台返回官网
|
||||
switchToWebsite(pageId = 'news') {
|
||||
// 隐藏 3D 容器
|
||||
this.appContainer.classList.add('hidden');
|
||||
|
||||
// 显示官网
|
||||
this.websiteContainer.classList.remove('hidden');
|
||||
|
||||
// 显示导航栏
|
||||
this.navbar.classList.remove('-translate-y-full');
|
||||
|
||||
// 切换到指定页面
|
||||
switchPage(pageId);
|
||||
}
|
||||
|
||||
// 转场完成回调(3D地球 → 2D地图)
|
||||
onTransitionComplete() {
|
||||
this.switchTo2D();
|
||||
}
|
||||
|
||||
// 切换到2D地图界面
|
||||
switchTo2D() {
|
||||
// 防止重复初始化:如果已经初始化过,直接返回
|
||||
if (this.is2DInitialized) {
|
||||
console.log('⏭️ 2D地图已初始化,跳过重复初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
// 隐藏3D容器和特效层
|
||||
this.canvasContainer.style.display = 'none';
|
||||
this.speedLines.style.display = 'none';
|
||||
|
||||
// 先显示地图容器(opacity: 0,但display: block)
|
||||
this.mapInterface.style.display = 'block';
|
||||
this.mapInterface.style.opacity = '0';
|
||||
|
||||
// 使用 requestAnimationFrame 确保浏览器完成布局渲染
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(async () => {
|
||||
// 现在容器已经有了正确的尺寸,可以安全初始化ECharts
|
||||
await this.mapController.init('china');
|
||||
|
||||
// 地图初始化完成后,立即调用resize确保尺寸正确
|
||||
this.mapController.resize();
|
||||
|
||||
// 标记2D地图已初始化,防止后续重复初始化
|
||||
this.is2DInitialized = true;
|
||||
|
||||
// 延迟淡入,提升视觉效果
|
||||
setTimeout(() => {
|
||||
gsap.to(this.mapInterface, { opacity: 1, duration: 1 });
|
||||
gsap.to(this.cloudFog, {
|
||||
opacity: 0,
|
||||
duration: 1,
|
||||
onComplete: () => {
|
||||
this.cloudFog.style.display = 'none';
|
||||
}
|
||||
});
|
||||
}, 50);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 显示企业列表
|
||||
async showList(cityName) {
|
||||
await this.mapController.hide();
|
||||
this.listController.show(cityName);
|
||||
}
|
||||
|
||||
// 显示企业详情
|
||||
async showDetail(company) {
|
||||
await this.listController.hide();
|
||||
this.detailController.show(company);
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
async backToList() {
|
||||
await this.detailController.hide();
|
||||
this.listController.show(this.listController.currentCity);
|
||||
}
|
||||
|
||||
// 返回地图
|
||||
async backToMap() {
|
||||
await this.listController.hide();
|
||||
this.mapController.show();
|
||||
this.mapController.resize(); // 确保地图尺寸正确
|
||||
}
|
||||
|
||||
// 绑定全局函数和事件
|
||||
bindGlobalFunctions() {
|
||||
// 地图界面logo点击返回官网
|
||||
if (this.mapLogoArea) {
|
||||
this.mapLogoArea.addEventListener('click', () => {
|
||||
this.switchToWebsite('news');
|
||||
});
|
||||
}
|
||||
|
||||
// 重置地图到全国视图
|
||||
window.resetMapToChina = () => {
|
||||
if (this.mapController) {
|
||||
this.mapController.reset();
|
||||
}
|
||||
};
|
||||
|
||||
// 从列表返回地图
|
||||
window.backToMap = () => {
|
||||
this.backToMap();
|
||||
};
|
||||
|
||||
// 从详情返回列表
|
||||
window.backToList = () => {
|
||||
this.backToList();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 应用启动
|
||||
const app = new App();
|
||||
127
js/scene/EarthModel.js
Normal file
127
js/scene/EarthModel.js
Normal file
@@ -0,0 +1,127 @@
|
||||
/* ===================================
|
||||
地球模型 - 包含地球、云层、大气层
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
|
||||
export class EarthModel {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.earthGroup = new THREE.Group();
|
||||
this.earth = null;
|
||||
this.clouds = null;
|
||||
this.atmosphere = null;
|
||||
this.loader = new THREE.TextureLoader();
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化地球模型
|
||||
init() {
|
||||
const radius = CONFIG.scene.earth.radius;
|
||||
const segments = CONFIG.scene.earth.segments;
|
||||
|
||||
// 创建地球
|
||||
this.earth = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(radius, segments, segments),
|
||||
new THREE.MeshPhongMaterial({
|
||||
map: this.loader.load(CONFIG.textures.earthMap),
|
||||
specularMap: this.loader.load(CONFIG.textures.earthSpecular),
|
||||
normalMap: this.loader.load(CONFIG.textures.earthNormal),
|
||||
specular: new THREE.Color(0x111111),
|
||||
shininess: 5
|
||||
})
|
||||
);
|
||||
this.earthGroup.add(this.earth);
|
||||
|
||||
// 创建云层
|
||||
this.clouds = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(CONFIG.scene.earth.cloudRadius, segments, segments),
|
||||
new THREE.MeshLambertMaterial({
|
||||
map: this.loader.load(CONFIG.textures.earthClouds),
|
||||
transparent: true,
|
||||
opacity: 0.9,
|
||||
blending: THREE.AdditiveBlending
|
||||
})
|
||||
);
|
||||
this.earthGroup.add(this.clouds);
|
||||
|
||||
// 创建大气光晕
|
||||
this.atmosphere = new THREE.Mesh(
|
||||
new THREE.SphereGeometry(CONFIG.scene.earth.atmosphereRadius, segments, segments),
|
||||
new THREE.ShaderMaterial({
|
||||
vertexShader: `
|
||||
varying vec3 vNormal;
|
||||
void main() {
|
||||
vNormal = normalize(normalMatrix * normal);
|
||||
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
varying vec3 vNormal;
|
||||
void main() {
|
||||
float intensity = pow(0.55 - dot(vNormal, vec3(0, 0, 1.0)), 4.5);
|
||||
gl_FragColor = vec4(0.0, 0.6, 1.0, 1.0) * intensity;
|
||||
}
|
||||
`,
|
||||
blending: THREE.AdditiveBlending,
|
||||
side: THREE.BackSide,
|
||||
transparent: true
|
||||
})
|
||||
);
|
||||
|
||||
// 大气层独立添加到场景(不在earthGroup中,避免被旋转)
|
||||
this.scene.add(this.atmosphere);
|
||||
this.scene.add(this.earthGroup);
|
||||
}
|
||||
|
||||
// 自动旋转地球和云层
|
||||
rotate() {
|
||||
if (this.earthGroup) {
|
||||
this.earthGroup.rotation.y += 0.0008;
|
||||
}
|
||||
if (this.clouds) {
|
||||
this.clouds.rotation.y += 0.0010;
|
||||
}
|
||||
}
|
||||
|
||||
// 获取可交互对象(用于射线检测)
|
||||
getInteractiveObjects() {
|
||||
return [this.earth, this.clouds];
|
||||
}
|
||||
|
||||
// 获取地球组(用于转场旋转)
|
||||
getGroup() {
|
||||
return this.earthGroup;
|
||||
}
|
||||
|
||||
// 获取大气层(用于悬停效果)
|
||||
getAtmosphere() {
|
||||
return this.atmosphere;
|
||||
}
|
||||
|
||||
// 获取云层(用于转场淡出)
|
||||
getClouds() {
|
||||
return this.clouds;
|
||||
}
|
||||
|
||||
// 销毁
|
||||
dispose() {
|
||||
if (this.earth) {
|
||||
this.earth.geometry.dispose();
|
||||
this.earth.material.dispose();
|
||||
}
|
||||
if (this.clouds) {
|
||||
this.clouds.geometry.dispose();
|
||||
this.clouds.material.dispose();
|
||||
}
|
||||
if (this.atmosphere) {
|
||||
this.atmosphere.geometry.dispose();
|
||||
this.atmosphere.material.dispose();
|
||||
this.scene.remove(this.atmosphere);
|
||||
}
|
||||
if (this.earthGroup) {
|
||||
this.scene.remove(this.earthGroup);
|
||||
}
|
||||
}
|
||||
}
|
||||
288
js/scene/SceneManager.js
Normal file
288
js/scene/SceneManager.js
Normal file
@@ -0,0 +1,288 @@
|
||||
/* ===================================
|
||||
3D场景管理器 - 主控制器
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
import { StarSystem } from './StarSystem.js';
|
||||
import { EarthModel } from './EarthModel.js';
|
||||
import { Transition } from './Transition.js';
|
||||
|
||||
export class SceneManager {
|
||||
constructor(container, onTransitionComplete) {
|
||||
this.container = container;
|
||||
this.onTransitionComplete = onTransitionComplete;
|
||||
|
||||
// 状态标志
|
||||
this.isIntro = true;
|
||||
this.isHovering = false;
|
||||
this.parallaxX = 0;
|
||||
this.parallaxY = 0;
|
||||
|
||||
// Three.js核心对象
|
||||
this.scene = null;
|
||||
this.camera = null;
|
||||
this.renderer = null;
|
||||
this.raycaster = new THREE.Raycaster();
|
||||
this.mouse = new THREE.Vector2();
|
||||
|
||||
// 子系统
|
||||
this.starSystem = null;
|
||||
this.earthModel = null;
|
||||
this.transition = null;
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 初始化场景
|
||||
init() {
|
||||
// 创建场景
|
||||
this.scene = new THREE.Scene();
|
||||
this.scene.fog = new THREE.FogExp2(CONFIG.scene.fog.color, CONFIG.scene.fog.density);
|
||||
|
||||
// 创建相机
|
||||
const camConfig = CONFIG.scene.camera;
|
||||
this.camera = new THREE.PerspectiveCamera(
|
||||
camConfig.fov,
|
||||
window.innerWidth / window.innerHeight,
|
||||
camConfig.near,
|
||||
camConfig.far
|
||||
);
|
||||
this.camera.position.z = camConfig.initialZ; // 开场动画:从深空开始
|
||||
|
||||
// 创建渲染器
|
||||
this.renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
this.renderer.setPixelRatio(window.devicePixelRatio);
|
||||
this.renderer.setClearColor(0x000000, 1); // 设置黑色背景
|
||||
this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
this.renderer.outputEncoding = THREE.sRGBEncoding;
|
||||
this.container.appendChild(this.renderer.domElement);
|
||||
|
||||
// 创建星空系统
|
||||
this.starSystem = new StarSystem(this.scene);
|
||||
|
||||
// 创建地球模型
|
||||
this.earthModel = new EarthModel(this.scene);
|
||||
|
||||
// 添加灯光
|
||||
this.setupLights();
|
||||
|
||||
// 绑定事件
|
||||
this.bindEvents();
|
||||
|
||||
// 启动动画循环
|
||||
this.animate();
|
||||
}
|
||||
|
||||
// 设置灯光
|
||||
setupLights() {
|
||||
const lights = CONFIG.scene.lights;
|
||||
|
||||
// 环境光
|
||||
this.scene.add(new THREE.AmbientLight(lights.ambient));
|
||||
|
||||
// 太阳光
|
||||
const sunLight = new THREE.DirectionalLight(lights.sun.color, lights.sun.intensity);
|
||||
sunLight.position.set(...lights.sun.position);
|
||||
this.scene.add(sunLight);
|
||||
|
||||
// 边缘光
|
||||
const rimLight = new THREE.SpotLight(lights.rim.color, lights.rim.intensity);
|
||||
rimLight.position.set(...lights.rim.position);
|
||||
rimLight.lookAt(0, 0, 0);
|
||||
this.scene.add(rimLight);
|
||||
}
|
||||
|
||||
// 开场动画
|
||||
startIntroSequence(uiElements) {
|
||||
const animConfig = CONFIG.animation.intro;
|
||||
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
this.isIntro = false; // 动画结束,允许交互
|
||||
}
|
||||
});
|
||||
|
||||
// A. 摄像机缓慢推进 (从 z=100 -> z=16)
|
||||
tl.to(this.camera.position, {
|
||||
z: CONFIG.scene.camera.defaultZ,
|
||||
duration: animConfig.cameraDuration,
|
||||
ease: "power2.out"
|
||||
});
|
||||
|
||||
// B. UI 淡入 (标题和提示依次出现)
|
||||
tl.to('.v1-title', {
|
||||
opacity: 1,
|
||||
duration: 1.5,
|
||||
ease: "power2.out"
|
||||
}, `-=${animConfig.titleDelay}`);
|
||||
|
||||
tl.to('.v1-subtitle', {
|
||||
opacity: 1,
|
||||
duration: 1.2,
|
||||
ease: "power2.out"
|
||||
}, `-=${animConfig.subtitleDelay}`);
|
||||
|
||||
tl.to('.instruction-hint', {
|
||||
opacity: 1,
|
||||
duration: 1.0,
|
||||
ease: "power2.out"
|
||||
}, `-=${animConfig.hintDelay}`);
|
||||
}
|
||||
|
||||
// 初始化转场系统
|
||||
initTransition(uiElements) {
|
||||
this.transition = new Transition(
|
||||
this.camera,
|
||||
this.earthModel,
|
||||
uiElements,
|
||||
this.onTransitionComplete
|
||||
);
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
bindEvents() {
|
||||
// 鼠标移动
|
||||
window.addEventListener('mousemove', (e) => this.onMouseMove(e));
|
||||
|
||||
// 鼠标点击
|
||||
window.addEventListener('mousedown', () => this.onMouseDown());
|
||||
|
||||
// 触摸移动(移动端)
|
||||
window.addEventListener('touchmove', (e) => this.onTouchMove(e), { passive: false });
|
||||
|
||||
// 触摸点击(移动端)
|
||||
window.addEventListener('touchstart', (e) => this.onTouchStart(e));
|
||||
|
||||
// 窗口resize
|
||||
window.addEventListener('resize', () => this.onWindowResize());
|
||||
}
|
||||
|
||||
// 鼠标移动事件
|
||||
onMouseMove(e) {
|
||||
if (this.transition && this.transition.isActive()) return;
|
||||
|
||||
this.mouse.x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
this.mouse.y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
// 只有开场动画结束后才检测悬停
|
||||
if (!this.isIntro) {
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
const intersects = this.raycaster.intersectObjects(
|
||||
this.earthModel.getInteractiveObjects()
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
if (!this.isHovering) {
|
||||
this.isHovering = true;
|
||||
this.container.classList.add('interactive');
|
||||
gsap.to(this.earthModel.getAtmosphere().scale, {
|
||||
x: 1.03,
|
||||
y: 1.03,
|
||||
z: 1.03,
|
||||
duration: 0.3
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (this.isHovering) {
|
||||
this.isHovering = false;
|
||||
this.container.classList.remove('interactive');
|
||||
gsap.to(this.earthModel.getAtmosphere().scale, {
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
duration: 0.3
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 视差效果
|
||||
this.parallaxX = (e.clientX - window.innerWidth / 2) * 0.001;
|
||||
this.parallaxY = (e.clientY - window.innerHeight / 2) * 0.001;
|
||||
}
|
||||
|
||||
// 鼠标点击事件
|
||||
onMouseDown() {
|
||||
if (this.isIntro || !this.isHovering) return;
|
||||
if (this.transition && this.transition.isActive()) return;
|
||||
|
||||
// 触发转场
|
||||
if (this.transition) {
|
||||
this.transition.trigger();
|
||||
}
|
||||
}
|
||||
|
||||
// 触摸移动事件(移动端)
|
||||
onTouchMove(e) {
|
||||
if (this.transition && this.transition.isActive()) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
|
||||
this.mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
// 视差效果
|
||||
this.parallaxX = (touch.clientX - window.innerWidth / 2) * 0.001;
|
||||
this.parallaxY = (touch.clientY - window.innerHeight / 2) * 0.001;
|
||||
}
|
||||
|
||||
// 触摸点击事件(移动端)
|
||||
onTouchStart(e) {
|
||||
if (this.isIntro) return;
|
||||
if (this.transition && this.transition.isActive()) return;
|
||||
|
||||
const touch = e.touches[0];
|
||||
this.mouse.x = (touch.clientX / window.innerWidth) * 2 - 1;
|
||||
this.mouse.y = -(touch.clientY / window.innerHeight) * 2 + 1;
|
||||
|
||||
// 检测是否点击地球
|
||||
this.raycaster.setFromCamera(this.mouse, this.camera);
|
||||
const intersects = this.raycaster.intersectObjects(
|
||||
this.earthModel.getInteractiveObjects()
|
||||
);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
// 触发转场
|
||||
if (this.transition) {
|
||||
this.transition.trigger();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 窗口resize
|
||||
onWindowResize() {
|
||||
this.camera.aspect = window.innerWidth / window.innerHeight;
|
||||
this.camera.updateProjectionMatrix();
|
||||
this.renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
}
|
||||
|
||||
// 动画循环
|
||||
animate() {
|
||||
requestAnimationFrame(() => this.animate());
|
||||
|
||||
// 如果不在转场中
|
||||
if (!this.transition || !this.transition.isActive()) {
|
||||
// 地球和云层旋转
|
||||
this.earthModel.rotate();
|
||||
|
||||
// 只有开场动画结束后才启用视差效果
|
||||
if (!this.isIntro) {
|
||||
this.camera.position.x += (this.parallaxX * 8 - this.camera.position.x) * 0.05;
|
||||
this.camera.position.y += (-this.parallaxY * 8 - this.camera.position.y) * 0.05;
|
||||
this.camera.lookAt(0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
this.renderer.render(this.scene, this.camera);
|
||||
}
|
||||
|
||||
// 销毁场景
|
||||
dispose() {
|
||||
if (this.starSystem) this.starSystem.dispose();
|
||||
if (this.earthModel) this.earthModel.dispose();
|
||||
if (this.renderer) {
|
||||
this.renderer.dispose();
|
||||
this.container.removeChild(this.renderer.domElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
100
js/scene/StarSystem.js
Normal file
100
js/scene/StarSystem.js
Normal file
@@ -0,0 +1,100 @@
|
||||
/* ===================================
|
||||
星空系统 - 彩色星星点缀
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
|
||||
export class StarSystem {
|
||||
constructor(scene) {
|
||||
this.scene = scene;
|
||||
this.stars = null;
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 创建星星纹理
|
||||
createStarTexture() {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 32;
|
||||
canvas.height = 32;
|
||||
const ctx = canvas.getContext('2d');
|
||||
const grad = ctx.createRadialGradient(16, 16, 0, 16, 16, 16);
|
||||
grad.addColorStop(0, 'rgba(255, 255, 255, 1)');
|
||||
grad.addColorStop(0.4, 'rgba(255, 255, 255, 0.4)');
|
||||
grad.addColorStop(1, 'rgba(255, 255, 255, 0)');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, 32, 32);
|
||||
return new THREE.Texture(canvas);
|
||||
}
|
||||
|
||||
// 初始化星空
|
||||
init() {
|
||||
const starTexture = this.createStarTexture();
|
||||
starTexture.needsUpdate = true;
|
||||
|
||||
const starGeo = new THREE.BufferGeometry();
|
||||
const starCount = CONFIG.scene.stars.count;
|
||||
const posArray = [];
|
||||
const colorArray = [];
|
||||
|
||||
// 颜色调色板
|
||||
const palette = CONFIG.scene.stars.colors.map(hex => new THREE.Color(hex));
|
||||
|
||||
// 生成星星位置和颜色
|
||||
for (let i = 0; i < starCount; i++) {
|
||||
posArray.push(
|
||||
(Math.random() - 0.5) * 500,
|
||||
(Math.random() - 0.5) * 500,
|
||||
(Math.random() - 0.5) * 250 - 50
|
||||
);
|
||||
const c = palette[Math.floor(Math.random() * palette.length)];
|
||||
colorArray.push(c.r, c.g, c.b);
|
||||
}
|
||||
|
||||
starGeo.setAttribute('position', new THREE.Float32BufferAttribute(posArray, 3));
|
||||
starGeo.setAttribute('color', new THREE.Float32BufferAttribute(colorArray, 3));
|
||||
|
||||
// 自定义着色器材质以支持彩色星星
|
||||
const starMaterial = new THREE.ShaderMaterial({
|
||||
uniforms: {
|
||||
pointTexture: { value: starTexture }
|
||||
},
|
||||
vertexShader: `
|
||||
attribute vec3 color;
|
||||
varying vec3 vColor;
|
||||
void main() {
|
||||
vColor = color;
|
||||
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
|
||||
gl_PointSize = ${CONFIG.scene.stars.size} * (300.0 / -mvPosition.z);
|
||||
gl_Position = projectionMatrix * mvPosition;
|
||||
}
|
||||
`,
|
||||
fragmentShader: `
|
||||
uniform sampler2D pointTexture;
|
||||
varying vec3 vColor;
|
||||
void main() {
|
||||
gl_FragColor = vec4(vColor, 1.0) * texture2D(pointTexture, gl_PointCoord);
|
||||
}
|
||||
`,
|
||||
blending: THREE.AdditiveBlending,
|
||||
depthWrite: false,
|
||||
transparent: true
|
||||
});
|
||||
|
||||
this.stars = new THREE.Points(starGeo, starMaterial);
|
||||
this.scene.add(this.stars);
|
||||
}
|
||||
|
||||
// 更新(预留)
|
||||
update() {
|
||||
// 可以在这里添加星星的动态效果,比如闪烁
|
||||
}
|
||||
|
||||
// 销毁
|
||||
dispose() {
|
||||
if (this.stars) {
|
||||
this.stars.geometry.dispose();
|
||||
this.stars.material.dispose();
|
||||
this.scene.remove(this.stars);
|
||||
}
|
||||
}
|
||||
}
|
||||
104
js/scene/Transition.js
Normal file
104
js/scene/Transition.js
Normal file
@@ -0,0 +1,104 @@
|
||||
/* ===================================
|
||||
转场特效 - 从3D地球到2D地图的超空间跳跃
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
|
||||
export class Transition {
|
||||
constructor(camera, earthModel, uiElements, onComplete) {
|
||||
this.camera = camera;
|
||||
this.earthModel = earthModel;
|
||||
this.uiElements = uiElements; // { uiLayer, hint, speedLines, cloudFog }
|
||||
this.onComplete = onComplete;
|
||||
this.isTransitioning = false;
|
||||
}
|
||||
|
||||
// 触发超空间跳跃转场
|
||||
trigger() {
|
||||
if (this.isTransitioning) return;
|
||||
this.isTransitioning = true;
|
||||
|
||||
// 获取UI元素
|
||||
const { uiLayer, hint, speedLines, cloudFog } = this.uiElements;
|
||||
|
||||
// 0. UI 消失
|
||||
uiLayer.style.opacity = 0;
|
||||
hint.style.opacity = 0;
|
||||
|
||||
const earthGroup = this.earthModel.getGroup();
|
||||
const clouds = this.earthModel.getClouds();
|
||||
const atmosphere = this.earthModel.getAtmosphere();
|
||||
|
||||
// 创建GSAP时间线
|
||||
const tl = gsap.timeline({
|
||||
onComplete: () => {
|
||||
this.isTransitioning = false;
|
||||
if (this.onComplete) this.onComplete();
|
||||
}
|
||||
});
|
||||
|
||||
// 转场配置
|
||||
const warpConfig = CONFIG.warp;
|
||||
const transConfig = CONFIG.animation.transition;
|
||||
|
||||
// 1. 地球对齐 + 速度线出现
|
||||
tl.to(earthGroup.rotation, {
|
||||
y: warpConfig.targetRotationY,
|
||||
x: warpConfig.targetRotationX,
|
||||
duration: transConfig.earthRotation,
|
||||
ease: "power2.inOut"
|
||||
}, "start");
|
||||
|
||||
tl.to(speedLines, {
|
||||
opacity: 1,
|
||||
scale: 1.5,
|
||||
duration: 1.0,
|
||||
ease: "power1.in"
|
||||
}, "start");
|
||||
|
||||
// 2. 镜头急速突进 (模拟穿过云层)
|
||||
tl.to(this.camera.position, {
|
||||
z: warpConfig.finalCameraZ,
|
||||
x: 0,
|
||||
y: 0,
|
||||
duration: transConfig.cameraZoom,
|
||||
ease: "expo.in",
|
||||
onUpdate: () => {
|
||||
// 镜头抖动效果 (Camera Shake) - 只在z<8时轻微抖动
|
||||
const z = this.camera.position.z;
|
||||
if (z < warpConfig.shakeThreshold.max && z > warpConfig.shakeThreshold.min) {
|
||||
const intensity = warpConfig.shakeIntensity;
|
||||
const shakeX = (Math.random() - 0.5) * intensity;
|
||||
const shakeY = (Math.random() - 0.5) * intensity;
|
||||
this.camera.position.x += shakeX;
|
||||
this.camera.position.y += shakeY;
|
||||
}
|
||||
}
|
||||
}, "start");
|
||||
|
||||
// 3. 材质变化:大气层和云层迅速变淡(为了不挡视线)
|
||||
tl.to([clouds.material, atmosphere.material], {
|
||||
opacity: 0,
|
||||
duration: 0.5
|
||||
}, "-=0.8");
|
||||
|
||||
// 4. "云雾吞噬"特效 (Backdrop Blur + Brightness)
|
||||
tl.to(cloudFog, {
|
||||
opacity: 1,
|
||||
scale: 1.2,
|
||||
backdropFilter: "blur(20px)",
|
||||
duration: transConfig.fogFade,
|
||||
ease: "power2.in"
|
||||
}, "-=0.8");
|
||||
}
|
||||
|
||||
// 重置转场状态
|
||||
reset() {
|
||||
this.isTransitioning = false;
|
||||
}
|
||||
|
||||
// 检查是否正在转场
|
||||
isActive() {
|
||||
return this.isTransitioning;
|
||||
}
|
||||
}
|
||||
243
js/ui/DetailInterface.js
Normal file
243
js/ui/DetailInterface.js
Normal file
@@ -0,0 +1,243 @@
|
||||
/* ===================================
|
||||
企业详情页面控制器
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
|
||||
export class DetailInterface {
|
||||
constructor(container, onBackClick) {
|
||||
this.container = container;
|
||||
this.onBackClick = onBackClick;
|
||||
this.currentCompany = null;
|
||||
|
||||
// Lightbox相关
|
||||
this.lightboxImages = [];
|
||||
this.currentImageIndex = 0;
|
||||
this.initLightbox();
|
||||
}
|
||||
|
||||
// 初始化Lightbox
|
||||
initLightbox() {
|
||||
this.lightbox = document.getElementById('image-lightbox');
|
||||
this.lightboxImage = document.getElementById('lightbox-image');
|
||||
this.lightboxClose = document.getElementById('lightbox-close');
|
||||
this.lightboxPrev = document.getElementById('lightbox-prev');
|
||||
this.lightboxNext = document.getElementById('lightbox-next');
|
||||
this.lightboxCurrent = document.getElementById('lightbox-current');
|
||||
this.lightboxTotal = document.getElementById('lightbox-total');
|
||||
|
||||
// 绑定事件
|
||||
this.lightboxClose.addEventListener('click', () => this.closeLightbox());
|
||||
this.lightboxPrev.addEventListener('click', () => this.prevImage());
|
||||
this.lightboxNext.addEventListener('click', () => this.nextImage());
|
||||
|
||||
// 点击背景关闭
|
||||
this.lightbox.addEventListener('click', (e) => {
|
||||
if (e.target === this.lightbox) {
|
||||
this.closeLightbox();
|
||||
}
|
||||
});
|
||||
|
||||
// 键盘控制
|
||||
this.keyboardHandler = (e) => {
|
||||
if (!this.lightbox.classList.contains('hidden')) {
|
||||
if (e.key === 'Escape') this.closeLightbox();
|
||||
if (e.key === 'ArrowLeft') this.prevImage();
|
||||
if (e.key === 'ArrowRight') this.nextImage();
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this.keyboardHandler);
|
||||
|
||||
// 触摸滑动支持(移动端)
|
||||
let touchStartX = 0;
|
||||
let touchEndX = 0;
|
||||
|
||||
this.lightbox.addEventListener('touchstart', (e) => {
|
||||
touchStartX = e.changedTouches[0].screenX;
|
||||
});
|
||||
|
||||
this.lightbox.addEventListener('touchend', (e) => {
|
||||
touchEndX = e.changedTouches[0].screenX;
|
||||
const diff = touchStartX - touchEndX;
|
||||
|
||||
if (Math.abs(diff) > 50) { // 滑动距离大于50px才触发
|
||||
if (diff > 0) {
|
||||
this.nextImage(); // 向左滑,下一张
|
||||
} else {
|
||||
this.prevImage(); // 向右滑,上一张
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 打开Lightbox
|
||||
openLightbox(images, index) {
|
||||
this.lightboxImages = images;
|
||||
this.currentImageIndex = index;
|
||||
this.lightboxTotal.textContent = images.length;
|
||||
this.showImage(index);
|
||||
this.lightbox.classList.remove('hidden');
|
||||
this.lightbox.classList.add('flex');
|
||||
|
||||
// 防止背景滚动
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
// 关闭Lightbox
|
||||
closeLightbox() {
|
||||
this.lightbox.classList.add('hidden');
|
||||
this.lightbox.classList.remove('flex');
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 显示指定图片
|
||||
showImage(index) {
|
||||
if (index < 0) index = this.lightboxImages.length - 1;
|
||||
if (index >= this.lightboxImages.length) index = 0;
|
||||
|
||||
this.currentImageIndex = index;
|
||||
this.lightboxImage.src = this.lightboxImages[index];
|
||||
this.lightboxCurrent.textContent = index + 1;
|
||||
|
||||
// 只有一张图片时隐藏左右按钮
|
||||
if (this.lightboxImages.length <= 1) {
|
||||
this.lightboxPrev.style.display = 'none';
|
||||
this.lightboxNext.style.display = 'none';
|
||||
} else {
|
||||
this.lightboxPrev.style.display = 'flex';
|
||||
this.lightboxNext.style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 上一张
|
||||
prevImage() {
|
||||
this.showImage(this.currentImageIndex - 1);
|
||||
}
|
||||
|
||||
// 下一张
|
||||
nextImage() {
|
||||
this.showImage(this.currentImageIndex + 1);
|
||||
}
|
||||
|
||||
// 显示企业详情
|
||||
show(company) {
|
||||
this.currentCompany = company;
|
||||
|
||||
// 填充左侧基本信息
|
||||
document.getElementById('d-name').innerText = company.name;
|
||||
document.getElementById('d-tags').innerHTML = company.tags
|
||||
.map(t => `<span class="tag-badge">${t}</span>`)
|
||||
.join(' ');
|
||||
document.getElementById('d-intro').innerText = company.intro;
|
||||
document.getElementById('d-reason').innerText = company.reason;
|
||||
document.getElementById('d-region').innerText = company.region;
|
||||
|
||||
// 填充相册
|
||||
this.renderGallery(company.gallery, company.shortName);
|
||||
|
||||
// 填充业务板块
|
||||
this.renderSegments(company.segments);
|
||||
|
||||
// 显示界面
|
||||
this.container.style.display = 'block';
|
||||
gsap.to(this.container, {
|
||||
opacity: 1,
|
||||
duration: CONFIG.animation.ui.fadeDuration
|
||||
});
|
||||
|
||||
// 业务板块进场动画
|
||||
gsap.to('.segment-card', {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.5,
|
||||
stagger: 0.05,
|
||||
delay: 0.2,
|
||||
ease: "power2.out"
|
||||
});
|
||||
}
|
||||
|
||||
// 渲染相册
|
||||
renderGallery(gallery, companyName) {
|
||||
const galContainer = document.getElementById('d-gallery');
|
||||
galContainer.innerHTML = '';
|
||||
|
||||
if (gallery && gallery.length > 0) {
|
||||
gallery.forEach((imgUrl, index) => {
|
||||
const img = document.createElement('img');
|
||||
img.src = imgUrl;
|
||||
img.className = 'gallery-img cursor-pointer hover:opacity-80 transition-opacity rounded-lg';
|
||||
img.alt = companyName + ' 相册';
|
||||
|
||||
// 添加点击事件,打开lightbox
|
||||
img.addEventListener('click', () => {
|
||||
this.openLightbox(gallery, index);
|
||||
});
|
||||
|
||||
galContainer.appendChild(img);
|
||||
});
|
||||
} else {
|
||||
galContainer.innerHTML = '<div class="col-span-3 text-gray-500 text-sm text-center py-4">暂无图片</div>';
|
||||
}
|
||||
}
|
||||
|
||||
// 渲染业务板块
|
||||
renderSegments(segments) {
|
||||
const segContainer = document.getElementById('d-segments');
|
||||
segContainer.innerHTML = '';
|
||||
|
||||
segments.forEach(seg => {
|
||||
const segDiv = document.createElement('div');
|
||||
segDiv.className = 'segment-card opacity-0 translate-y-4';
|
||||
|
||||
const jobsHtml = seg.jobs
|
||||
.map(j => `<span class="job-tag">${j}</span>`)
|
||||
.join('');
|
||||
|
||||
segDiv.innerHTML = `
|
||||
<div class="segment-title">${seg.name}</div>
|
||||
<div class="flex flex-wrap">${jobsHtml}</div>
|
||||
`;
|
||||
|
||||
segContainer.appendChild(segDiv);
|
||||
});
|
||||
}
|
||||
|
||||
// 隐藏详情界面
|
||||
hide() {
|
||||
return new Promise(resolve => {
|
||||
gsap.to(this.container, {
|
||||
opacity: 0,
|
||||
duration: CONFIG.animation.ui.fadeDuration,
|
||||
onComplete: () => {
|
||||
this.container.style.display = 'none';
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 返回列表
|
||||
async backToList() {
|
||||
await this.hide();
|
||||
if (this.onBackClick) {
|
||||
this.onBackClick();
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁
|
||||
dispose() {
|
||||
this.currentCompany = null;
|
||||
const galContainer = document.getElementById('d-gallery');
|
||||
const segContainer = document.getElementById('d-segments');
|
||||
if (galContainer) galContainer.innerHTML = '';
|
||||
if (segContainer) segContainer.innerHTML = '';
|
||||
|
||||
// 清理键盘事件监听器
|
||||
if (this.keyboardHandler) {
|
||||
document.removeEventListener('keydown', this.keyboardHandler);
|
||||
}
|
||||
|
||||
// 关闭lightbox
|
||||
this.closeLightbox();
|
||||
}
|
||||
}
|
||||
135
js/ui/ListInterface.js
Normal file
135
js/ui/ListInterface.js
Normal file
@@ -0,0 +1,135 @@
|
||||
/* ===================================
|
||||
企业列表界面控制器
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
import { companiesData } from '../data.js';
|
||||
|
||||
export class ListInterface {
|
||||
constructor(container, onCompanyClick, onBackClick) {
|
||||
this.container = container;
|
||||
this.onCompanyClick = onCompanyClick;
|
||||
this.onBackClick = onBackClick;
|
||||
this.currentCity = '';
|
||||
this.cardsContainer = document.getElementById('cards-container');
|
||||
}
|
||||
|
||||
// 显示指定城市的企业列表
|
||||
show(cityName) {
|
||||
this.currentCity = cityName;
|
||||
|
||||
console.log('=== ListInterface.show 调试 ===');
|
||||
console.log('要显示的城市:', cityName);
|
||||
console.log('所有企业数量:', companiesData.length);
|
||||
console.log('所有城市:', [...new Set(companiesData.map(c => c.city))].join(', '));
|
||||
|
||||
// 设置标题
|
||||
document.getElementById('list-city-title').innerText = cityName + ' · 企业名录';
|
||||
|
||||
// 过滤该城市的企业数据
|
||||
let cityCompanies = companiesData.filter(c => c.city === cityName);
|
||||
|
||||
console.log('过滤后的企业数量:', cityCompanies.length);
|
||||
if (cityCompanies.length === 0) {
|
||||
console.warn('⚠️ 没有找到该城市的企业数据!');
|
||||
}
|
||||
|
||||
// 更新左侧统计
|
||||
document.getElementById('company-count').innerText = cityCompanies.length;
|
||||
const jobCount = cityCompanies.reduce(
|
||||
(sum, c) => sum + c.segments.reduce((s, seg) => s + seg.jobs.length, 0),
|
||||
0
|
||||
);
|
||||
document.getElementById('job-count').innerText = jobCount;
|
||||
|
||||
// 清空旧数据
|
||||
this.cardsContainer.innerHTML = '';
|
||||
|
||||
// 渲染企业卡片
|
||||
cityCompanies.forEach((company) => {
|
||||
const card = this.createCompanyCard(company);
|
||||
this.cardsContainer.appendChild(card);
|
||||
});
|
||||
|
||||
// 显示界面
|
||||
this.container.style.display = 'block';
|
||||
gsap.to(this.container, {
|
||||
opacity: 1,
|
||||
duration: CONFIG.animation.ui.fadeDuration
|
||||
});
|
||||
|
||||
// 卡片进场动画
|
||||
gsap.to('.company-card', {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.6,
|
||||
stagger: CONFIG.animation.ui.cardStagger,
|
||||
delay: CONFIG.animation.ui.cardDelay,
|
||||
ease: "power2.out"
|
||||
});
|
||||
}
|
||||
|
||||
// 创建企业卡片
|
||||
createCompanyCard(company) {
|
||||
const card = document.createElement('div');
|
||||
card.className = 'company-card rounded-xl overflow-hidden cursor-pointer opacity-0 translate-y-10';
|
||||
card.onclick = () => {
|
||||
if (this.onCompanyClick) {
|
||||
this.onCompanyClick(company);
|
||||
}
|
||||
};
|
||||
|
||||
// 生成标签HTML
|
||||
const tagsHtml = company.tags.map(t => `<span class="tag-badge">${t}</span>`).join(' ');
|
||||
|
||||
// 生成职位预览
|
||||
const jobsPreview = company.segments[0].jobs.slice(0, 3).join(' / ');
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="h-40 w-full overflow-hidden relative">
|
||||
<img src="${company.cover}" class="w-full h-full object-cover" alt="${company.shortName}">
|
||||
<div class="absolute bottom-0 left-0 w-full h-1/2 bg-gradient-to-t from-gray-900 to-transparent"></div>
|
||||
</div>
|
||||
<div class="p-5 text-white">
|
||||
<h3 class="font-bold text-lg mb-2">${company.shortName}</h3>
|
||||
<div class="flex flex-wrap gap-2 mb-3">${tagsHtml}</div>
|
||||
<p class="text-xs text-gray-400 line-clamp-2 mb-3">${company.intro}</p>
|
||||
<div class="pt-3 border-t border-white/10 flex justify-between items-center">
|
||||
<span class="text-xs text-gray-500">热招: ${jobsPreview}...</span>
|
||||
<span class="text-cyan-400 text-xs font-bold">详情 →</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
// 隐藏列表界面
|
||||
hide() {
|
||||
return new Promise(resolve => {
|
||||
gsap.to(this.container, {
|
||||
opacity: 0,
|
||||
duration: CONFIG.animation.ui.fadeDuration,
|
||||
onComplete: () => {
|
||||
this.container.style.display = 'none';
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 返回地图
|
||||
async backToMap() {
|
||||
await this.hide();
|
||||
if (this.onBackClick) {
|
||||
this.onBackClick();
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁
|
||||
dispose() {
|
||||
if (this.cardsContainer) {
|
||||
this.cardsContainer.innerHTML = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
333
js/ui/MapInterface.js
Normal file
333
js/ui/MapInterface.js
Normal file
@@ -0,0 +1,333 @@
|
||||
/* ===================================
|
||||
地图界面控制器 - 使用ECharts渲染中国地图
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
|
||||
// 省份名称到地图配置的映射
|
||||
const PROVINCE_MAP = {
|
||||
'江苏省': { key: 'jiangsu', code: '320000', name: '江苏省' },
|
||||
'浙江省': { key: 'zhejiang', code: '330000', name: '浙江省' },
|
||||
'广东省': { key: 'guangdong', code: '440000', name: '广东省' },
|
||||
'山东省': { key: 'shandong', code: '370000', name: '山东省' },
|
||||
'河北省': { key: 'hebei', code: '130000', name: '河北省' },
|
||||
'河南省': { key: 'henan', code: '410000', name: '河南省' },
|
||||
'四川省': { key: 'sichuan', code: '510000', name: '四川省' },
|
||||
'湖北省': { key: 'hubei', code: '420000', name: '湖北省' },
|
||||
'湖南省': { key: 'hunan', code: '430000', name: '湖南省' },
|
||||
'安徽省': { key: 'anhui', code: '340000', name: '安徽省' },
|
||||
'福建省': { key: 'fujian', code: '350000', name: '福建省' },
|
||||
'陕西省': { key: 'shaanxi', code: '610000', name: '陕西省' },
|
||||
'辽宁省': { key: 'liaoning', code: '210000', name: '辽宁省' },
|
||||
'北京市': { key: 'beijing', code: '110000', name: '北京市' },
|
||||
'上海市': { key: 'shanghai', code: '310000', name: '上海市' },
|
||||
'天津市': { key: 'tianjin', code: '120000', name: '天津市' },
|
||||
'重庆市': { key: 'chongqing', code: '500000', name: '重庆市' }
|
||||
};
|
||||
|
||||
export class MapInterface {
|
||||
constructor(container, onCityClick) {
|
||||
this.container = container;
|
||||
this.onCityClick = onCityClick;
|
||||
this.myChart = null;
|
||||
this.currentRegion = 'china';
|
||||
this.currentProvinceName = ''; // 当前省份名称
|
||||
}
|
||||
|
||||
// 初始化地图
|
||||
async init(regionName = 'china', provinceName = '') {
|
||||
this.currentRegion = regionName;
|
||||
this.currentProvinceName = provinceName;
|
||||
|
||||
// 直辖市特殊处理:不加载省级地图,直接显示企业列表
|
||||
const municipalities = ['北京市', '上海市', '天津市', '重庆市'];
|
||||
if (regionName === 'province' && municipalities.includes(provinceName)) {
|
||||
console.log(`直辖市 ${provinceName},跳过省级地图,直接显示企业列表`);
|
||||
// 直接调用城市点击回调
|
||||
if (this.onCityClick) {
|
||||
this.onCityClick(provinceName);
|
||||
}
|
||||
return; // 终止地图加载
|
||||
}
|
||||
|
||||
// 如果已有实例,先销毁
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
}
|
||||
|
||||
// 创建图表实例
|
||||
const chartDom = document.getElementById('map-chart');
|
||||
if (!chartDom) {
|
||||
console.error('地图容器元素不存在');
|
||||
return;
|
||||
}
|
||||
|
||||
this.myChart = echarts.init(chartDom, null, {
|
||||
renderer: 'canvas',
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight - 64 // 减去导航栏高度
|
||||
});
|
||||
|
||||
// 显示加载动画
|
||||
this.myChart.showLoading({
|
||||
text: CONFIG.echarts.loadingText,
|
||||
color: CONFIG.echarts.loadingColor,
|
||||
textColor: CONFIG.echarts.loadingTextColor,
|
||||
maskColor: CONFIG.echarts.loadingMaskColor
|
||||
});
|
||||
|
||||
// 确定地图配置
|
||||
const mapConfig = CONFIG.map;
|
||||
const isProvince = (regionName !== 'china');
|
||||
let geoJsonUrl = '';
|
||||
|
||||
if (regionName === 'china') {
|
||||
// 全国地图
|
||||
geoJsonUrl = mapConfig.geoJsonUrls.china;
|
||||
document.getElementById('breadcrumb-container').classList.add('hidden');
|
||||
document.getElementById('top-region-name').innerText = '全国';
|
||||
} else {
|
||||
// 省级地图 - 动态加载
|
||||
const provinceInfo = PROVINCE_MAP[provinceName];
|
||||
if (provinceInfo) {
|
||||
// 优先使用config中配置的URL,否则使用动态生成的URL
|
||||
geoJsonUrl = mapConfig.geoJsonUrls[provinceInfo.key] ||
|
||||
`https://geo.datav.aliyun.com/areas_v3/bound/${provinceInfo.code}_full.json`;
|
||||
|
||||
// 更新面包屑和顶部区域名称
|
||||
const breadcrumb = document.getElementById('breadcrumb-container');
|
||||
if (breadcrumb) {
|
||||
breadcrumb.innerHTML = `
|
||||
<span class="hover:text-white cursor-pointer transition" onclick="window.resetMapToChina()">全国</span>
|
||||
<span class="mx-2 text-gray-600">/</span>
|
||||
<span class="text-cyan-400 font-bold">${provinceInfo.name}</span>
|
||||
`;
|
||||
breadcrumb.classList.remove('hidden');
|
||||
}
|
||||
document.getElementById('top-region-name').innerText = provinceInfo.name;
|
||||
} else {
|
||||
console.error(`未找到省份 "${provinceName}" 的配置`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取GeoJSON数据
|
||||
const res = await fetch(geoJsonUrl);
|
||||
|
||||
// 检查响应状态,防止404导致JSON解析报错
|
||||
if (!res.ok) {
|
||||
throw new Error(`地图数据请求失败: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
|
||||
const geoJson = await res.json();
|
||||
this.myChart.hideLoading();
|
||||
|
||||
// 注册地图
|
||||
echarts.registerMap(regionName, geoJson);
|
||||
|
||||
// 调试:输出应该高亮的省份/城市
|
||||
if (!isProvince) {
|
||||
console.log('=== 全国地图应该高亮的省份 ===');
|
||||
console.log(mapConfig.activeProvinces);
|
||||
} else {
|
||||
console.log('=== 省级地图应该高亮的城市 ===');
|
||||
console.log(mapConfig.activeCities);
|
||||
}
|
||||
|
||||
// 调试:输出地图返回的所有区域名称
|
||||
console.log('=== 地图GeoJSON返回的区域名称 ===');
|
||||
console.log(geoJson.features.map(f => f.properties.name));
|
||||
|
||||
// 生成数据列表(高亮有数据的省份/城市)
|
||||
const dataList = geoJson.features.map(feature => {
|
||||
const name = feature.properties.name;
|
||||
|
||||
let hasData = false;
|
||||
if (isProvince) {
|
||||
// 省级地图 - 检查城市是否有数据
|
||||
// 处理城市名称格式:地图可能返回"苏州",数据中是"苏州市"
|
||||
let cityName = name;
|
||||
if (!cityName.endsWith('市') && !cityName.includes('自治') && !cityName.includes('地区')) {
|
||||
cityName += '市';
|
||||
}
|
||||
hasData = mapConfig.activeCities.includes(cityName);
|
||||
if (hasData) {
|
||||
console.log(`✅ 高亮城市: ${name} (处理后: ${cityName})`);
|
||||
}
|
||||
} else {
|
||||
// 全国地图 - 检查省份是否有数据
|
||||
hasData = mapConfig.activeProvinces.includes(name);
|
||||
if (hasData) {
|
||||
console.log(`✅ 高亮省份: ${name}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: name,
|
||||
itemStyle: {
|
||||
// 有数据的区域:金黄色高亮
|
||||
areaColor: hasData ? 'rgba(251, 191, 36, 0.35)' : 'rgba(11, 16, 38, 0.8)',
|
||||
borderColor: hasData ? '#fbbf24' : '#1e293b',
|
||||
borderWidth: hasData ? 2 : 0.5
|
||||
},
|
||||
emphasis: {
|
||||
itemStyle: {
|
||||
// 鼠标悬停:更亮的金黄色
|
||||
areaColor: hasData ? 'rgba(251, 191, 36, 0.6)' : 'rgba(30, 41, 59, 0.9)'
|
||||
},
|
||||
label: {
|
||||
color: hasData ? '#fff' : '#94a3b8',
|
||||
fontWeight: hasData ? 'bold' : 'normal'
|
||||
}
|
||||
},
|
||||
disabled: !hasData
|
||||
};
|
||||
});
|
||||
|
||||
// 配置地图选项
|
||||
const isMobile = window.innerWidth < 768;
|
||||
this.myChart.setOption({
|
||||
geo: {
|
||||
map: regionName,
|
||||
roam: true,
|
||||
zoom: isMobile ? 1.3 : mapConfig.defaultZoom, // 移动端适配缩放
|
||||
scaleLimit: {
|
||||
min: isMobile ? 1.0 : 0.8, // 移动端最小100%,PC端80%
|
||||
max: isMobile ? 2.0 : 2.5 // 移动端最大200%,PC端250%
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
color: '#94a3b8',
|
||||
fontSize: isMobile ? 8 : 10 // 移动端使用更小字体
|
||||
},
|
||||
itemStyle: {
|
||||
areaColor: '#0f172a',
|
||||
borderColor: '#1e293b'
|
||||
},
|
||||
select: { disabled: true }
|
||||
},
|
||||
series: [{
|
||||
type: 'map',
|
||||
geoIndex: 0,
|
||||
data: dataList
|
||||
}]
|
||||
});
|
||||
|
||||
// 核心修复:立即强制重绘,解决移动端白屏问题
|
||||
this.myChart.resize();
|
||||
|
||||
// 绑定点击事件
|
||||
this.myChart.on('click', params => {
|
||||
if (params.data && params.data.disabled) return;
|
||||
|
||||
console.log('地图点击:', params.name, '当前层级:', isProvince ? '省级' : '全国');
|
||||
|
||||
if (!isProvince) {
|
||||
// 全国地图 - 点击省份
|
||||
const clickedProvince = params.name;
|
||||
console.log('🗺️ 全国地图点击事件触发');
|
||||
console.log('点击省份:', clickedProvince, '是否有数据:', mapConfig.activeProvinces.includes(clickedProvince));
|
||||
|
||||
// 直辖市列表 - 直接显示企业列表,不进入省级地图
|
||||
const municipalities = ['北京市', '上海市', '天津市', '重庆市'];
|
||||
|
||||
console.log('🔍 检查是否为直辖市:', municipalities.includes(clickedProvince));
|
||||
|
||||
if (municipalities.includes(clickedProvince)) {
|
||||
// 直辖市:直接显示企业列表
|
||||
console.log('✅ 确认是直辖市,直接显示企业列表');
|
||||
if (this.onCityClick) {
|
||||
console.log('📞 调用 onCityClick:', clickedProvince);
|
||||
this.onCityClick(clickedProvince);
|
||||
}
|
||||
} else if (mapConfig.activeProvinces.includes(clickedProvince)) {
|
||||
// 其他省份:下钻到省级地图
|
||||
console.log('🌍 普通省份,下钻到省级地图');
|
||||
this.init('province', clickedProvince);
|
||||
}
|
||||
} else {
|
||||
// 省级地图 - 点击城市
|
||||
let cityName = params.name;
|
||||
|
||||
// 处理城市名称格式:确保以"市"结尾
|
||||
if (!cityName.endsWith('市') && !cityName.includes('自治') && !cityName.includes('地区')) {
|
||||
cityName += '市';
|
||||
}
|
||||
|
||||
console.log('点击城市:', params.name, '处理后:', cityName, '是否有数据:', mapConfig.activeCities.includes(cityName));
|
||||
|
||||
if (mapConfig.activeCities.includes(cityName)) {
|
||||
// 跳转到企业列表页
|
||||
if (this.onCityClick) {
|
||||
this.onCityClick(cityName);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('地图加载严重错误:', error);
|
||||
this.myChart.hideLoading();
|
||||
|
||||
// 显示错误提示
|
||||
this.myChart.setOption({
|
||||
title: {
|
||||
text: '地图数据加载失败',
|
||||
subtext: '请检查网络连接后刷新页面',
|
||||
left: 'center',
|
||||
top: 'center',
|
||||
textStyle: {
|
||||
color: '#fff',
|
||||
fontSize: 18
|
||||
},
|
||||
subtextStyle: {
|
||||
color: '#999',
|
||||
fontSize: 14
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 重置到全国地图
|
||||
reset() {
|
||||
this.init('china');
|
||||
}
|
||||
|
||||
// 显示地图界面
|
||||
show() {
|
||||
this.container.style.display = 'block';
|
||||
gsap.to(this.container, { opacity: 1, duration: CONFIG.animation.ui.fadeDuration });
|
||||
}
|
||||
|
||||
// 隐藏地图界面
|
||||
hide() {
|
||||
return new Promise(resolve => {
|
||||
gsap.to(this.container, {
|
||||
opacity: 0,
|
||||
duration: CONFIG.animation.ui.fadeDuration,
|
||||
onComplete: () => {
|
||||
this.container.style.display = 'none';
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 调整大小
|
||||
resize() {
|
||||
if (this.myChart) {
|
||||
this.myChart.resize();
|
||||
}
|
||||
}
|
||||
|
||||
// 销毁
|
||||
dispose() {
|
||||
if (this.myChart) {
|
||||
this.myChart.dispose();
|
||||
this.myChart = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
/* Version: 1763779096 */
|
||||
328
js/ui/SearchController.js
Normal file
328
js/ui/SearchController.js
Normal file
@@ -0,0 +1,328 @@
|
||||
/* ===================================
|
||||
搜索控制器
|
||||
=================================== */
|
||||
|
||||
import { companiesData, activeCities, activeProvinces } from '../data.js';
|
||||
import { UIUtils } from './UIUtils.js';
|
||||
|
||||
export class SearchController {
|
||||
constructor(inputElement, callbacks) {
|
||||
this.input = inputElement;
|
||||
this.callbacks = callbacks; // { onSelectProvince, onSelectCity, onSelectCompany }
|
||||
|
||||
// 创建下拉容器
|
||||
this.suggestionsContainer = this.createSuggestionsContainer();
|
||||
this.input.parentElement.appendChild(this.suggestionsContainer);
|
||||
|
||||
// 当前选中的索引
|
||||
this.selectedIndex = -1;
|
||||
this.currentResults = [];
|
||||
|
||||
// 初始化事件监听
|
||||
this.init();
|
||||
}
|
||||
|
||||
// 创建下拉建议容器
|
||||
createSuggestionsContainer() {
|
||||
const container = document.createElement('div');
|
||||
container.className = 'search-suggestions hidden';
|
||||
return container;
|
||||
}
|
||||
|
||||
// 初始化事件监听
|
||||
init() {
|
||||
// 输入事件 - 使用防抖
|
||||
const debouncedSearch = UIUtils.debounce((e) => {
|
||||
const keyword = e.target.value.trim();
|
||||
if (keyword) {
|
||||
this.performSearch(keyword);
|
||||
} else {
|
||||
this.hideSuggestions();
|
||||
}
|
||||
}, 300);
|
||||
|
||||
this.input.addEventListener('input', debouncedSearch);
|
||||
|
||||
// 键盘事件
|
||||
this.input.addEventListener('keydown', (e) => this.handleKeyboard(e));
|
||||
|
||||
// 失去焦点时延迟关闭(给点击建议留时间)
|
||||
this.input.addEventListener('blur', () => {
|
||||
setTimeout(() => this.hideSuggestions(), 200);
|
||||
});
|
||||
|
||||
// 获得焦点时如果有内容则重新搜索
|
||||
this.input.addEventListener('focus', () => {
|
||||
const keyword = this.input.value.trim();
|
||||
if (keyword) {
|
||||
this.performSearch(keyword);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 执行搜索
|
||||
performSearch(keyword) {
|
||||
const results = this.search(keyword);
|
||||
this.currentResults = results;
|
||||
this.selectedIndex = -1;
|
||||
this.renderSuggestions(results);
|
||||
}
|
||||
|
||||
// 模糊搜索算法
|
||||
search(keyword) {
|
||||
const lowerKeyword = keyword.toLowerCase();
|
||||
const results = {
|
||||
provinces: [],
|
||||
cities: [],
|
||||
companies: []
|
||||
};
|
||||
|
||||
// 搜索省份
|
||||
activeProvinces.forEach(province => {
|
||||
const score = this.calculateMatchScore(lowerKeyword, province.toLowerCase());
|
||||
if (score > 0) {
|
||||
results.provinces.push({ name: province, score });
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索城市
|
||||
activeCities.forEach(city => {
|
||||
const score = this.calculateMatchScore(lowerKeyword, city.toLowerCase());
|
||||
if (score > 0) {
|
||||
results.cities.push({ name: city, score });
|
||||
}
|
||||
});
|
||||
|
||||
// 搜索企业
|
||||
companiesData.forEach(company => {
|
||||
const nameScore = this.calculateMatchScore(lowerKeyword, company.name.toLowerCase());
|
||||
const shortNameScore = this.calculateMatchScore(lowerKeyword, company.shortName.toLowerCase());
|
||||
const score = Math.max(nameScore, shortNameScore);
|
||||
|
||||
if (score > 0) {
|
||||
results.companies.push({
|
||||
name: company.shortName,
|
||||
fullName: company.name,
|
||||
city: company.city,
|
||||
data: company,
|
||||
score
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 按分数排序
|
||||
results.provinces.sort((a, b) => b.score - a.score);
|
||||
results.cities.sort((a, b) => b.score - a.score);
|
||||
results.companies.sort((a, b) => b.score - a.score);
|
||||
|
||||
// 限制结果数量:省份2条、城市3条、企业5条,总计最多8条
|
||||
results.provinces = results.provinces.slice(0, 2);
|
||||
results.cities = results.cities.slice(0, 3);
|
||||
results.companies = results.companies.slice(0, 5);
|
||||
|
||||
// 再次确保总数不超过8条
|
||||
const total = results.provinces.length + results.cities.length + results.companies.length;
|
||||
if (total > 8) {
|
||||
// 优先保证省份和城市,然后企业
|
||||
const remaining = 8 - results.provinces.length - results.cities.length;
|
||||
if (remaining < results.companies.length) {
|
||||
results.companies = results.companies.slice(0, remaining);
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
// 计算匹配分数
|
||||
calculateMatchScore(keyword, target) {
|
||||
// 去除"省"、"市"等后缀进行匹配
|
||||
const cleanTarget = target.replace(/省$|市$/g, '');
|
||||
const cleanKeyword = keyword.replace(/省$|市$/g, '');
|
||||
|
||||
// 完全匹配
|
||||
if (cleanTarget === cleanKeyword || target === keyword) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
// 开头匹配
|
||||
if (cleanTarget.startsWith(cleanKeyword) || target.startsWith(keyword)) {
|
||||
return 80;
|
||||
}
|
||||
|
||||
// 包含匹配
|
||||
if (cleanTarget.includes(cleanKeyword) || target.includes(keyword)) {
|
||||
return 60;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 渲染搜索建议
|
||||
renderSuggestions(results) {
|
||||
const { provinces, cities, companies } = results;
|
||||
const total = provinces.length + cities.length + companies.length;
|
||||
|
||||
if (total === 0) {
|
||||
this.suggestionsContainer.innerHTML = '<div class="search-no-result">未找到相关结果</div>';
|
||||
this.showSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
|
||||
// 渲染省份
|
||||
provinces.forEach((item, index) => {
|
||||
html += this.createSuggestionItem('province', item.name, null, index);
|
||||
});
|
||||
|
||||
// 渲染城市
|
||||
cities.forEach((item, index) => {
|
||||
const globalIndex = provinces.length + index;
|
||||
html += this.createSuggestionItem('city', item.name, null, globalIndex);
|
||||
});
|
||||
|
||||
// 渲染企业
|
||||
companies.forEach((item, index) => {
|
||||
const globalIndex = provinces.length + cities.length + index;
|
||||
html += this.createSuggestionItem('company', item.name, item.city, globalIndex);
|
||||
});
|
||||
|
||||
this.suggestionsContainer.innerHTML = html;
|
||||
this.showSuggestions();
|
||||
|
||||
// 绑定点击事件
|
||||
this.bindClickEvents();
|
||||
}
|
||||
|
||||
// 创建单个建议项
|
||||
createSuggestionItem(type, name, city, index) {
|
||||
const icons = {
|
||||
province: '🗺️',
|
||||
city: '🏙️',
|
||||
company: '🏢'
|
||||
};
|
||||
|
||||
const cityTag = city ? `<span class="search-city-tag">${city}</span>` : '';
|
||||
|
||||
return `
|
||||
<div class="search-item" data-index="${index}" data-type="${type}">
|
||||
<span class="search-icon">${icons[type]}</span>
|
||||
<span class="search-name">${name}</span>
|
||||
${cityTag}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 绑定点击事件
|
||||
bindClickEvents() {
|
||||
const items = this.suggestionsContainer.querySelectorAll('.search-item');
|
||||
items.forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const index = parseInt(item.dataset.index);
|
||||
this.selectItem(index);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 选择项目
|
||||
selectItem(index) {
|
||||
const { provinces, cities, companies } = this.currentResults;
|
||||
const provincesCount = provinces.length;
|
||||
const citiesCount = cities.length;
|
||||
|
||||
let type, item;
|
||||
|
||||
if (index < provincesCount) {
|
||||
type = 'province';
|
||||
item = provinces[index];
|
||||
} else if (index < provincesCount + citiesCount) {
|
||||
type = 'city';
|
||||
item = cities[index - provincesCount];
|
||||
} else {
|
||||
type = 'company';
|
||||
item = companies[index - provincesCount - citiesCount];
|
||||
}
|
||||
|
||||
this.executeSelection(type, item);
|
||||
this.hideSuggestions();
|
||||
this.input.value = '';
|
||||
}
|
||||
|
||||
// 执行选择
|
||||
executeSelection(type, item) {
|
||||
switch (type) {
|
||||
case 'province':
|
||||
if (this.callbacks.onSelectProvince) {
|
||||
this.callbacks.onSelectProvince(item.name);
|
||||
}
|
||||
break;
|
||||
case 'city':
|
||||
if (this.callbacks.onSelectCity) {
|
||||
this.callbacks.onSelectCity(item.name);
|
||||
}
|
||||
break;
|
||||
case 'company':
|
||||
if (this.callbacks.onSelectCompany) {
|
||||
this.callbacks.onSelectCompany(item.data);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 键盘事件处理
|
||||
handleKeyboard(e) {
|
||||
const items = this.suggestionsContainer.querySelectorAll('.search-item');
|
||||
const totalItems = items.length;
|
||||
|
||||
if (totalItems === 0) return;
|
||||
|
||||
switch (e.key) {
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = (this.selectedIndex + 1) % totalItems;
|
||||
this.updateSelection(items);
|
||||
break;
|
||||
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
this.selectedIndex = (this.selectedIndex - 1 + totalItems) % totalItems;
|
||||
this.updateSelection(items);
|
||||
break;
|
||||
|
||||
case 'Enter':
|
||||
e.preventDefault();
|
||||
if (this.selectedIndex >= 0 && this.selectedIndex < totalItems) {
|
||||
this.selectItem(this.selectedIndex);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'Escape':
|
||||
this.hideSuggestions();
|
||||
this.input.blur();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 更新选中状态
|
||||
updateSelection(items) {
|
||||
items.forEach((item, index) => {
|
||||
if (index === this.selectedIndex) {
|
||||
item.classList.add('active');
|
||||
item.scrollIntoView({ block: 'nearest' });
|
||||
} else {
|
||||
item.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 显示建议
|
||||
showSuggestions() {
|
||||
this.suggestionsContainer.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 隐藏建议
|
||||
hideSuggestions() {
|
||||
this.suggestionsContainer.classList.add('hidden');
|
||||
this.selectedIndex = -1;
|
||||
}
|
||||
}
|
||||
201
js/ui/UIUtils.js
Normal file
201
js/ui/UIUtils.js
Normal file
@@ -0,0 +1,201 @@
|
||||
/* ===================================
|
||||
UI工具函数
|
||||
=================================== */
|
||||
|
||||
import { CONFIG } from '../config.js';
|
||||
|
||||
export class UIUtils {
|
||||
/**
|
||||
* 切换界面显示/隐藏
|
||||
* @param {HTMLElement} hideElement - 要隐藏的元素
|
||||
* @param {HTMLElement} showElement - 要显示的元素
|
||||
* @param {Function} onComplete - 完成回调
|
||||
*/
|
||||
static switchInterface(hideElement, showElement, onComplete) {
|
||||
const duration = CONFIG.animation.ui.fadeDuration;
|
||||
|
||||
gsap.to(hideElement, {
|
||||
opacity: 0,
|
||||
duration: duration,
|
||||
onComplete: () => {
|
||||
hideElement.style.display = 'none';
|
||||
showElement.style.display = 'block';
|
||||
gsap.to(showElement, {
|
||||
opacity: 1,
|
||||
duration: duration,
|
||||
onComplete: onComplete
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量显示元素(带渐入动画)
|
||||
* @param {string} selector - CSS选择器
|
||||
* @param {object} options - 动画选项
|
||||
*/
|
||||
static staggerShow(selector, options = {}) {
|
||||
const defaults = {
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
duration: 0.6,
|
||||
stagger: CONFIG.animation.ui.cardStagger,
|
||||
delay: CONFIG.animation.ui.cardDelay,
|
||||
ease: "power2.out"
|
||||
};
|
||||
|
||||
gsap.to(selector, { ...defaults, ...options });
|
||||
}
|
||||
|
||||
/**
|
||||
* 移动端触摸事件适配
|
||||
* @param {HTMLElement} element - 目标元素
|
||||
* @param {object} handlers - 事件处理器 { onTap, onSwipe }
|
||||
*/
|
||||
static bindTouchEvents(element, handlers) {
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
let startTime = 0;
|
||||
|
||||
element.addEventListener('touchstart', (e) => {
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
startTime = Date.now();
|
||||
});
|
||||
|
||||
element.addEventListener('touchend', (e) => {
|
||||
const endX = e.changedTouches[0].clientX;
|
||||
const endY = e.changedTouches[0].clientY;
|
||||
const endTime = Date.now();
|
||||
|
||||
const deltaX = endX - startX;
|
||||
const deltaY = endY - startY;
|
||||
const deltaTime = endTime - startTime;
|
||||
|
||||
// 判断是点击还是滑动
|
||||
if (Math.abs(deltaX) < 10 && Math.abs(deltaY) < 10 && deltaTime < 300) {
|
||||
// 点击
|
||||
if (handlers.onTap) handlers.onTap(e);
|
||||
} else {
|
||||
// 滑动
|
||||
if (handlers.onSwipe) {
|
||||
const direction = Math.abs(deltaX) > Math.abs(deltaY)
|
||||
? (deltaX > 0 ? 'right' : 'left')
|
||||
: (deltaY > 0 ? 'down' : 'up');
|
||||
handlers.onSwipe(direction, e);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param {Function} func - 要防抖的函数
|
||||
* @param {number} wait - 等待时间(毫秒)
|
||||
* @returns {Function}
|
||||
*/
|
||||
static debounce(func, wait = 300) {
|
||||
let timeout;
|
||||
return function executedFunction(...args) {
|
||||
const later = () => {
|
||||
clearTimeout(timeout);
|
||||
func(...args);
|
||||
};
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(later, wait);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 节流函数
|
||||
* @param {Function} func - 要节流的函数
|
||||
* @param {number} limit - 限制间隔(毫秒)
|
||||
* @returns {Function}
|
||||
*/
|
||||
static throttle(func, limit = 100) {
|
||||
let inThrottle;
|
||||
return function executedFunction(...args) {
|
||||
if (!inThrottle) {
|
||||
func(...args);
|
||||
inThrottle = true;
|
||||
setTimeout(() => inThrottle = false, limit);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测是否为移动设备
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isMobile() {
|
||||
return window.innerWidth <= CONFIG.mobile.breakpoint;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取安全区域内边距
|
||||
* @returns {object} { top, bottom, left, right }
|
||||
*/
|
||||
static getSafeAreaInsets() {
|
||||
const computed = getComputedStyle(document.documentElement);
|
||||
return {
|
||||
top: parseInt(computed.getPropertyValue('env(safe-area-inset-top)')) || 0,
|
||||
bottom: parseInt(computed.getPropertyValue('env(safe-area-inset-bottom)')) || 0,
|
||||
left: parseInt(computed.getPropertyValue('env(safe-area-inset-left)')) || 0,
|
||||
right: parseInt(computed.getPropertyValue('env(safe-area-inset-right)')) || 0
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化陀螺仪控制(移动端视差)
|
||||
* @param {Function} callback - 回调函数,接收 (beta, gamma) 参数
|
||||
*/
|
||||
static initGyroscope(callback) {
|
||||
if (!this.isMobile()) return;
|
||||
|
||||
if (window.DeviceOrientationEvent) {
|
||||
window.addEventListener('deviceorientation', (e) => {
|
||||
const beta = e.beta || 0; // 前后倾斜(-180 ~ 180)
|
||||
const gamma = e.gamma || 0; // 左右倾斜(-90 ~ 90)
|
||||
callback(beta, gamma);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 显示加载提示
|
||||
* @param {string} text - 提示文本
|
||||
*/
|
||||
static showLoading(text = '加载中...') {
|
||||
const existing = document.getElementById('loading-overlay');
|
||||
if (existing) return;
|
||||
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'loading-overlay';
|
||||
overlay.style.cssText = `
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 9999;
|
||||
color: #38bdf8;
|
||||
font-size: 1rem;
|
||||
`;
|
||||
overlay.innerText = text;
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
|
||||
/**
|
||||
* 隐藏加载提示
|
||||
*/
|
||||
static hideLoading() {
|
||||
const overlay = document.getElementById('loading-overlay');
|
||||
if (overlay) {
|
||||
overlay.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
463
js/website.js
Normal file
463
js/website.js
Normal file
@@ -0,0 +1,463 @@
|
||||
/* ===================================
|
||||
企业官网逻辑模块
|
||||
=================================== */
|
||||
|
||||
// 轮播图数据(从CSV加载)
|
||||
let newsData = [];
|
||||
|
||||
// 菜单状态
|
||||
let isMenuOpen = false;
|
||||
let currentCarouselIndex = 0;
|
||||
let carouselInterval = null;
|
||||
|
||||
/**
|
||||
* 初始化官网功能
|
||||
* @param {Function} navigateToApp - 切换到3D平台的回调函数
|
||||
*/
|
||||
export async function initWebsite(navigateToApp) {
|
||||
// 1. 加载CSV数据
|
||||
await loadNewsData();
|
||||
|
||||
// 2. 初始化汉堡菜单
|
||||
initMenu(navigateToApp);
|
||||
|
||||
// 3. 初始化轮播图
|
||||
initCarousel();
|
||||
|
||||
// 4. 默认显示新闻中心
|
||||
switchPage('news');
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载CSV数据
|
||||
*/
|
||||
async function loadNewsData() {
|
||||
try {
|
||||
const response = await fetch('./网站数据_新闻内容_所有记录.csv');
|
||||
const csvText = await response.text();
|
||||
|
||||
// 解析CSV
|
||||
const lines = csvText.trim().split('\n');
|
||||
// 跳过标题行
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
// 使用正则表达式处理CSV(处理可能包含逗号的字段)
|
||||
const matches = line.match(/(?:^|,)("(?:[^"]|"")*"|[^,]*)/g);
|
||||
if (matches && matches.length >= 3) {
|
||||
const title = matches[0].replace(/^,?"?|"?$/g, '').trim();
|
||||
const link = matches[1].replace(/^,?"?|"?$/g, '').trim();
|
||||
const imageUrl = matches[2].replace(/^,?"?|"?$/g, '').trim();
|
||||
|
||||
newsData.push({
|
||||
title,
|
||||
link,
|
||||
imageUrl,
|
||||
isVideo: link.endsWith('.mp4') // 根据link判断是否是视频
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
console.log('加载了', newsData.length, '条新闻数据');
|
||||
} catch (error) {
|
||||
console.error('加载新闻数据失败:', error);
|
||||
// 使用默认数据
|
||||
newsData = [{
|
||||
title: '加载失败',
|
||||
link: '',
|
||||
imageUrl: 'https://via.placeholder.com/1200x600/1e293b/38bdf8?text=加载失败',
|
||||
isVideo: false
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化汉堡菜单逻辑
|
||||
*/
|
||||
function initMenu(navigateToApp) {
|
||||
const menuBtn = document.getElementById('menu-btn');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
const menuLinks = document.querySelectorAll('.menu-link');
|
||||
const navLogoArea = document.getElementById('nav-logo-area');
|
||||
|
||||
// 点击logo区域返回首页
|
||||
if (navLogoArea) {
|
||||
navLogoArea.style.cursor = 'pointer';
|
||||
navLogoArea.addEventListener('click', () => {
|
||||
switchPage('news');
|
||||
});
|
||||
}
|
||||
|
||||
// 汉堡按钮点击事件
|
||||
menuBtn.addEventListener('click', toggleMenu);
|
||||
|
||||
// 菜单项点击事件
|
||||
menuLinks.forEach(link => {
|
||||
link.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const target = link.dataset.target;
|
||||
|
||||
// 关闭菜单
|
||||
closeMenu();
|
||||
|
||||
// 路由导航
|
||||
if (target === 'app') {
|
||||
// 切换到 3D 内推平台
|
||||
navigateToApp();
|
||||
} else {
|
||||
// 切换官网子页面
|
||||
switchPage(target);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 点击菜单背景关闭菜单
|
||||
mobileMenu.addEventListener('click', (e) => {
|
||||
if (e.target === mobileMenu) {
|
||||
closeMenu();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换菜单开关状态
|
||||
*/
|
||||
function toggleMenu() {
|
||||
const menuBtn = document.getElementById('menu-btn');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
isMenuOpen = !isMenuOpen;
|
||||
|
||||
if (isMenuOpen) {
|
||||
mobileMenu.classList.remove('translate-x-full');
|
||||
menuBtn.innerHTML = '<i class="fa-solid fa-xmark"></i>';
|
||||
} else {
|
||||
mobileMenu.classList.add('translate-x-full');
|
||||
menuBtn.innerHTML = '<i class="fa-solid fa-bars"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭菜单
|
||||
*/
|
||||
function closeMenu() {
|
||||
const menuBtn = document.getElementById('menu-btn');
|
||||
const mobileMenu = document.getElementById('mobile-menu');
|
||||
|
||||
isMenuOpen = false;
|
||||
mobileMenu.classList.add('translate-x-full');
|
||||
menuBtn.innerHTML = '<i class="fa-solid fa-bars"></i>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 页面切换
|
||||
* @param {string} pageId - 页面ID (news/about/product)
|
||||
*/
|
||||
export function switchPage(pageId) {
|
||||
const websiteContainer = document.getElementById('website-container');
|
||||
const appContainer = document.getElementById('app-container');
|
||||
const navbar = document.getElementById('navbar');
|
||||
|
||||
// 确保官网容器显示,3D容器隐藏
|
||||
websiteContainer.classList.remove('hidden');
|
||||
appContainer.classList.add('hidden');
|
||||
|
||||
// 显示导航栏
|
||||
navbar.classList.remove('-translate-y-full');
|
||||
|
||||
// 隐藏所有页面
|
||||
document.querySelectorAll('.page-section').forEach(sec => {
|
||||
sec.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 显示目标页面
|
||||
const targetPage = document.getElementById(`page-${pageId}`);
|
||||
if (targetPage) {
|
||||
targetPage.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// 更新菜单高亮状态
|
||||
updateMenuHighlight(pageId);
|
||||
|
||||
// 重新启动轮播图(如果是新闻页面)
|
||||
if (pageId === 'news') {
|
||||
startCarousel();
|
||||
} else {
|
||||
stopCarousel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新菜单高亮状态
|
||||
* @param {string} activePageId - 当前激活的页面ID
|
||||
*/
|
||||
function updateMenuHighlight(activePageId) {
|
||||
const menuLinks = document.querySelectorAll('.menu-link');
|
||||
menuLinks.forEach(link => {
|
||||
const target = link.dataset.target;
|
||||
|
||||
if (target === activePageId) {
|
||||
// 激活状态:白色粗体
|
||||
link.classList.remove('text-gray-400');
|
||||
link.classList.add('text-white', 'font-bold');
|
||||
} else if (target === 'app') {
|
||||
// 内推平台保持青色
|
||||
link.classList.remove('text-white', 'font-bold');
|
||||
link.classList.add('text-cyan-400');
|
||||
} else {
|
||||
// 非激活状态:灰色
|
||||
link.classList.remove('text-white', 'font-bold');
|
||||
link.classList.add('text-gray-400');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化轮播图
|
||||
*/
|
||||
function initCarousel() {
|
||||
const track = document.getElementById('carousel-track');
|
||||
const dotsContainer = document.getElementById('carousel-dots');
|
||||
|
||||
if (!track || !dotsContainer) return;
|
||||
|
||||
// 清空现有内容
|
||||
track.innerHTML = '';
|
||||
dotsContainer.innerHTML = '';
|
||||
|
||||
// 生成轮播项
|
||||
newsData.forEach((item, index) => {
|
||||
// 创建轮播项容器
|
||||
const carouselItem = document.createElement('div');
|
||||
carouselItem.className = 'carousel-item';
|
||||
carouselItem.style.cursor = 'pointer';
|
||||
|
||||
if (item.isVideo) {
|
||||
// 视频项:显示视频封面/缩略图
|
||||
const videoPreview = document.createElement('img');
|
||||
videoPreview.src = item.imageUrl; // 使用CSV中的缩略图URL
|
||||
videoPreview.className = 'carousel-img';
|
||||
videoPreview.alt = item.title;
|
||||
|
||||
// 添加播放图标
|
||||
const playIcon = document.createElement('div');
|
||||
playIcon.className = 'video-play-icon';
|
||||
playIcon.innerHTML = '<i class="fa-solid fa-play"></i>';
|
||||
|
||||
carouselItem.appendChild(videoPreview);
|
||||
carouselItem.appendChild(playIcon);
|
||||
|
||||
// 点击播放视频
|
||||
carouselItem.addEventListener('click', () => {
|
||||
openVideoModal(item.link, item.title); // 使用CSV中的视频URL
|
||||
});
|
||||
} else {
|
||||
// 图片项
|
||||
const img = document.createElement('img');
|
||||
img.src = item.imageUrl;
|
||||
img.className = 'carousel-img';
|
||||
img.alt = item.title;
|
||||
carouselItem.appendChild(img);
|
||||
|
||||
// 点击跳转链接
|
||||
if (item.link) {
|
||||
carouselItem.addEventListener('click', () => {
|
||||
window.open(item.link, '_blank');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 添加标题覆盖层
|
||||
const titleOverlay = document.createElement('div');
|
||||
titleOverlay.className = 'carousel-title-overlay';
|
||||
titleOverlay.textContent = item.title;
|
||||
carouselItem.appendChild(titleOverlay);
|
||||
|
||||
track.appendChild(carouselItem);
|
||||
|
||||
// 生成指示点
|
||||
const dot = document.createElement('div');
|
||||
dot.className = `dot ${index === 0 ? 'active' : ''}`;
|
||||
dot.addEventListener('click', () => {
|
||||
goToSlide(index);
|
||||
});
|
||||
dotsContainer.appendChild(dot);
|
||||
});
|
||||
|
||||
// 添加触摸滑动功能
|
||||
initTouchSwipe(track);
|
||||
|
||||
// 启动自动播放
|
||||
startCarousel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化触摸滑动功能
|
||||
*/
|
||||
function initTouchSwipe(track) {
|
||||
let startX = 0;
|
||||
let isDragging = false;
|
||||
|
||||
track.addEventListener('touchstart', (e) => {
|
||||
startX = e.touches[0].clientX;
|
||||
isDragging = true;
|
||||
stopCarousel(); // 暂停自动播放
|
||||
});
|
||||
|
||||
track.addEventListener('touchmove', (e) => {
|
||||
if (!isDragging) return;
|
||||
// 阻止默认滚动行为
|
||||
e.preventDefault();
|
||||
}, { passive: false });
|
||||
|
||||
track.addEventListener('touchend', (e) => {
|
||||
if (!isDragging) return;
|
||||
isDragging = false;
|
||||
|
||||
const endX = e.changedTouches[0].clientX;
|
||||
const diffX = startX - endX;
|
||||
|
||||
// 滑动阈值:50像素
|
||||
if (Math.abs(diffX) > 50) {
|
||||
if (diffX > 0) {
|
||||
// 向左滑动 - 下一张
|
||||
currentCarouselIndex = (currentCarouselIndex + 1) % newsData.length;
|
||||
} else {
|
||||
// 向右滑动 - 上一张
|
||||
currentCarouselIndex = (currentCarouselIndex - 1 + newsData.length) % newsData.length;
|
||||
}
|
||||
goToSlide(currentCarouselIndex);
|
||||
}
|
||||
|
||||
// 恢复自动播放
|
||||
startCarousel();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到指定幻灯片
|
||||
* @param {number} index - 幻灯片索引
|
||||
*/
|
||||
function goToSlide(index) {
|
||||
const track = document.getElementById('carousel-track');
|
||||
const dotsContainer = document.getElementById('carousel-dots');
|
||||
|
||||
if (!track || !dotsContainer) return;
|
||||
|
||||
currentCarouselIndex = index;
|
||||
|
||||
// 更新轮播图位置
|
||||
track.style.transform = `translateX(-${index * 100}%)`;
|
||||
|
||||
// 更新指示点
|
||||
Array.from(dotsContainer.children).forEach((dot, i) => {
|
||||
if (i === index) {
|
||||
dot.classList.add('active');
|
||||
} else {
|
||||
dot.classList.remove('active');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动轮播图自动播放
|
||||
*/
|
||||
function startCarousel() {
|
||||
// 清除之前的定时器
|
||||
if (carouselInterval) {
|
||||
clearInterval(carouselInterval);
|
||||
}
|
||||
|
||||
// 每3秒切换一次
|
||||
carouselInterval = setInterval(() => {
|
||||
currentCarouselIndex = (currentCarouselIndex + 1) % newsData.length;
|
||||
goToSlide(currentCarouselIndex);
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止轮播图自动播放
|
||||
*/
|
||||
function stopCarousel() {
|
||||
if (carouselInterval) {
|
||||
clearInterval(carouselInterval);
|
||||
carouselInterval = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 打开视频播放模态窗口
|
||||
*/
|
||||
function openVideoModal(videoUrl, title) {
|
||||
// 创建模态窗口
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'video-modal';
|
||||
modal.className = 'video-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="video-modal-content">
|
||||
<div class="video-modal-header">
|
||||
<h3>${title}</h3>
|
||||
<button class="video-modal-close">
|
||||
<i class="fa-solid fa-xmark"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="video-modal-body">
|
||||
<video controls autoplay style="width: 100%; max-height: 70vh;">
|
||||
<source src="${videoUrl}" type="video/mp4">
|
||||
您的浏览器不支持视频播放。
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// 关闭按钮事件
|
||||
const closeBtn = modal.querySelector('.video-modal-close');
|
||||
closeBtn.addEventListener('click', () => {
|
||||
closeVideoModal();
|
||||
});
|
||||
|
||||
// 点击背景关闭
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) {
|
||||
closeVideoModal();
|
||||
}
|
||||
});
|
||||
|
||||
// 显示模态窗口
|
||||
setTimeout(() => {
|
||||
modal.classList.add('active');
|
||||
}, 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭视频播放模态窗口
|
||||
*/
|
||||
function closeVideoModal() {
|
||||
const modal = document.getElementById('video-modal');
|
||||
if (modal) {
|
||||
modal.classList.remove('active');
|
||||
setTimeout(() => {
|
||||
modal.remove();
|
||||
}, 300);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新轮播图数据(供外部调用)
|
||||
* @param {Array<Object>} newData - 新的新闻数据数组
|
||||
*/
|
||||
export function updateCarouselData(newData) {
|
||||
if (!Array.isArray(newData) || newData.length === 0) {
|
||||
console.warn('Invalid carousel data provided');
|
||||
return;
|
||||
}
|
||||
|
||||
// 更新数据
|
||||
newsData.length = 0;
|
||||
newsData.push(...newData);
|
||||
|
||||
// 重新初始化
|
||||
currentCarouselIndex = 0;
|
||||
initCarousel();
|
||||
}
|
||||
Reference in New Issue
Block a user