初始化多多畅职企业内推平台项目

功能特性:
- 3D地球动画与中国地图可视化
- 省份/城市/企业搜索功能
- 308家企业数据展示
- 响应式设计(PC端和移动端)
- 企业详情页面与业务板块展示
- 官网新闻轮播图

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
KQL
2025-11-22 19:38:14 +08:00
commit ab50931347
41 changed files with 56412 additions and 0 deletions

243
js/ui/DetailInterface.js Normal file
View 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
View 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
View 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
View 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
View 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();
}
}
}