2025-07-25 19:31:28 +08:00

1073 lines
24 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="home-page">
<div class="page-header">
<h1>欢迎来到红色警戒3数据分析中心</h1>
<div class="header-subtitle">
<span class="date-range">探索地图工具和赛事的综合平台</span>
</div>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading-container">
<div class="loading-spinner"></div>
<p>正在加载数据...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<div class="error-icon"></div>
<p>{{ error }}</p>
<button @click="initializeData" class="retry-btn">重新加载</button>
</div>
<!-- 主要内容 -->
<div v-else>
<!-- 搜索地图区域 -->
<div class="search-section">
<div class="search-container">
<div class="search-box">
<input
type="text"
v-model="searchQuery"
placeholder="搜索地图名称、作者或标签..."
@keyup.enter="handleSearch"
class="search-input"
>
<button @click="handleSearch" class="search-btn">
<i class="fas fa-search"></i>
</button>
</div>
<div class="search-tags">
<span class="tag-label">热门标签</span>
<span
v-for="tag in popularTags"
:key="tag"
class="search-tag"
@click="searchByTag(tag)"
>
{{ tag }}
</span>
</div>
</div>
</div>
<div class="content-grid">
<!-- 左侧主要内容 -->
<div class="main-content">
<!-- 地图作者推荐 -->
<div class="section-card">
<div class="section-header">
<h2>
<i class="fas fa-star"></i>
活跃作者推荐
</h2>
<router-link to="/author" class="more-link">查看更多</router-link>
</div>
<div class="authors-grid">
<div
v-for="author in recommendedAuthors"
:key="author.id"
class="author-card"
@click="viewAuthorMaps(author.username)"
>
<div class="author-info">
<h4>{{ author.username }}</h4>
<p>积分{{ author.credits }}</p>
<div class="author-stats">
<span><i class="fas fa-calendar"></i> 三月活跃{{ author.three_month_live ? '是' : '否' }}</span>
<span><i class="fas fa-clock"></i> 一月活跃{{ author.one_month_live ? '是' : '否' }}</span>
</div>
</div>
</div>
</div>
</div>
<!-- 在线工具 -->
<div class="section-card">
<div class="section-header">
<h2>
<i class="fas fa-tools"></i>
在线工具
</h2>
</div>
<div class="tools-grid">
<div class="tool-card" @click="navigateToTool('/weapon-match', ['lv-admin','lv-mod'])">
<div class="tool-icon">
<i class="fas fa-crosshairs"></i>
</div>
<h4>Weapon 匹配</h4>
<p>武器配置匹配工具</p>
<span class="tool-badge mod">需要模组权限</span>
</div>
<div class="tool-card" @click="navigateToTool('/configEditor', ['lv-admin','lv-mod'])">
<div class="tool-icon">
<i class="fas fa-edit"></i>
</div>
<h4>Config 编辑器</h4>
<p>配置文件编辑工具</p>
<span class="tool-badge mod">需要模组权限</span>
</div>
<div class="tool-card" @click="navigateToTool('/PIC2TGA', ['lv-admin','lv-mod','lv-map','lv-competitor'])">
<div class="tool-icon">
<i class="fas fa-image"></i>
</div>
<h4>图像转换</h4>
<p>PIC转TGA格式工具</p>
<span class="tool-badge map">需要地图权限</span>
</div>
<div class="tool-card" @click="navigateToTool('/terrainGenerate', ['lv-admin','lv-mod','lv-map','lv-competitor'])">
<div class="tool-icon">
<i class="fas fa-layer-group"></i>
</div>
<h4>地形合成</h4>
<p>地形纹理合成工具</p>
<span class="tool-badge map">需要地图权限</span>
</div>
</div>
</div>
<!-- 地形与纹理 -->
<div class="section-card">
<div class="section-header">
<h2>
<i class="fas fa-mountain"></i>
地形与纹理
</h2>
<router-link to="/terrain" class="more-link">查看更多</router-link>
</div>
<div class="terrain-grid">
<div
v-for="terrain in featuredTerrains"
:key="terrain.id"
class="terrain-card"
@click="viewTerrain(terrain.id)"
>
<img :src="terrain.preview" :alt="terrain.name" class="terrain-image">
</div>
</div>
</div>
</div>
<!-- 右侧边栏 -->
<div class="sidebar">
<!-- 赛事信息 -->
<div class="section-card">
<div class="section-header">
<h3>
<i class="fas fa-trophy"></i>
最新赛事
</h3>
<router-link to="/competition" class="more-link">查看全部</router-link>
</div>
<div class="competitions-list">
<div
v-for="competition in latestCompetitions"
:key="competition.id"
class="competition-item"
@click="viewCompetition(competition.id)"
>
<div class="competition-status" :class="competition.status">
{{ getStatusText(competition.status) }}
</div>
<h4>{{ competition.name }}</h4>
<div class="competition-time">
<i class="fas fa-calendar"></i>
{{ formatDate(competition.start_time) }}
</div>
<p>主办方{{ competition.organizer }}</p>
</div>
</div>
</div>
<!-- 办事大厅 -->
<div class="section-card">
<div class="section-header">
<h3>
<i class="fas fa-bullhorn"></i>
办事大厅
</h3>
</div>
<div class="demands-list">
<div
v-for="demand in visibleDemands"
:key="demand.id"
class="demand-item"
@click="viewDemand(demand.id)"
>
<div class="demand-header">
<h4 class="demand-title">{{ demand.content }}</h4>
<span class="demand-reward" :class="demand.reward && demand.reward !== '无' && demand.reward !== '0' && demand.reward !== '' ? 'has-reward' : 'no-reward'">
{{ demand.reward && demand.reward !== '无' && demand.reward !== '0' && demand.reward !== '' ? '有悬赏' : '无悬赏' }}
</span>
</div>
<div class="demand-meta">
<span class="demand-requester">请求人{{ demand.requester || '匿名' }}</span>
<span class="demand-date">{{ formatDate(demand.date) }}</span>
</div>
<div v-if="demand.reward && demand.reward !== '无' && demand.reward !== '0' && demand.reward !== ''" class="demand-bounty">悬赏{{ demand.reward }}</div>
</div>
</div>
<div v-if="normalDemands.length > 5" class="demands-toggle-btn-wrapper">
<button class="demands-toggle-btn" @click="showAllDemands = !showAllDemands">
{{ showAllDemands ? '收起' : '查看更多' }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRouter } from 'vue-router'
import { getUserInfo } from '@/utils/jwt'
import { hasPrivilegeWithTemp } from '@/utils/privilege'
import { getMapEditors } from '@/api/centre_maps.js'
import { getTournamentList } from '@/api/tournament.js'
import { getMaps, getAllTags } from '@/api/maps.js'
import { getDemandsList } from '@/api/demands.js'
import '../../assets/styles/common.css'
const router = useRouter()
// 加载状态
const loading = ref(true)
const error = ref('')
// 搜索相关
const searchQuery = ref('')
const popularTags = ref(['PVP', '生存', '对战', '合作', '塔防', '竞技'])
// 推荐作者
const recommendedAuthors = ref([])
// 精选地形
const featuredTerrains = ref([])
// 获取地形数据
const fetchTerrains = async () => {
try {
const response = await fetch('https://api.zybdatasupport.online/terrain')
if (!response.ok) {
throw new Error('获取地形数据失败')
}
const data = await response.json()
// 过滤出图片文件并取前8个作为4x2网格显示
const imageFiles = data.filter(item => {
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']
const lowerKey = item.key.toLowerCase()
return imageExtensions.some(ext => lowerKey.endsWith(ext))
})
// 取前8个地形图只保留必要信息
featuredTerrains.value = imageFiles.slice(0, 8).map((terrain, index) => ({
id: index + 1,
name: terrain.key.split('.')[0],
preview: `http://dataimg-1307694021.cos.ap-beijing.myqcloud.com/Terrain/jpg/${terrain.key}`
}))
} catch (error) {
console.error('获取地形数据失败:', error)
// 如果API失败不显示任何地形图
featuredTerrains.value = []
}
}
// 最新赛事
const latestCompetitions = ref([])
// 办事大厅需求列表
const announcements = ref([])
// 控制展开/收起
const showAllDemands = ref(false)
// 只显示 NORMAL 状态的需求
const normalDemands = computed(() => announcements.value.filter(d => d.status === 'NORMAL'))
const visibleDemands = computed(() => showAllDemands.value ? normalDemands.value : normalDemands.value.slice(0, 5))
// 获取需求列表(按时间从新到旧排序)
const fetchDemands = async () => {
try {
const demands = await getDemandsList()
// 假设demands有date字段按date降序排列
announcements.value = demands
.slice()
.sort((a, b) => new Date(b.date) - new Date(a.date))
.map((item, idx) => ({
id: item.id || idx + 1,
date: item.date,
content: item.content || '无标题',
reward: item.reward || '无',
requester: item.requester || '匿名',
status: item.status || 'NORMAL' // 假设status字段
}))
} catch (error) {
announcements.value = []
}
}
// 获取地图作者数据
const fetchAuthors = async () => {
try {
const authors = await getMapEditors()
// 取前3名活跃作者保留原始数据结构
const topAuthors = authors.slice(0, 3).map((author, index) => ({
id: index + 1,
username: author.update_editor,
credits: author.credits,
three_month_live: author.three_month_live,
one_month_live: author.one_month_live
}))
recommendedAuthors.value = topAuthors
} catch (error) {
console.error('获取作者数据失败:', error)
}
}
// 获取赛事数据
const fetchCompetitions = async () => {
try {
const competitions = await getTournamentList()
// 取最新的3个赛事
latestCompetitions.value = competitions.slice(0, 3).map(comp => ({
id: comp.id,
name: comp.name,
status: comp.status,
start_time: comp.start_time,
organizer: comp.organizer
}))
} catch (error) {
console.error('获取赛事数据失败:', error)
// 如果API失败不显示任何赛事
latestCompetitions.value = []
}
}
// 获取热门标签
const fetchPopularTags = async () => {
try {
const tags = await getAllTags()
if (tags && tags.length > 0) {
// 取前6个标签作为热门标签
popularTags.value = tags.slice(0, 6)
}
} catch (error) {
console.error('获取标签失败:', error)
// 保持默认标签
}
}
// 方法
const handleSearch = () => {
if (searchQuery.value.trim()) {
router.push(`/maps?search=${encodeURIComponent(searchQuery.value)}`)
}
}
const searchByTag = (tag) => {
router.push(`/maps?tags=${encodeURIComponent(tag)}`)
}
const viewAuthorMaps = (username) => {
// 跳转到editors-maps页面并查询该作者的地图
router.push(`/editors-maps?author=${encodeURIComponent(username)}`)
}
const navigateToTool = async (path, requiredPrivileges) => {
try {
const userInfo = await getUserInfo()
if (!userInfo) {
router.push('/backend/login')
return
}
if (requiredPrivileges && !hasPrivilegeWithTemp(userInfo, requiredPrivileges)) {
// 显示权限不足提示
alert('权限不足,需要相应权限才能使用此工具')
return
}
router.push(path)
} catch (error) {
router.push('/backend/login')
}
}
const viewTerrain = (id) => {
router.push(`/terrain?id=${id}`)
}
const viewCompetition = (id) => {
router.push(`/competition/detail?id=${id}`)
}
const viewDemand = (id) => {
router.push(`/demands?id=${id}`)
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString || dateString === 'Test_date') return '日期未提供'
try {
const date = new Date(dateString)
if (isNaN(date.getTime())) {
return dateString
}
const pad = num => num.toString().padStart(2, '0')
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`
} catch (e) {
return dateString
}
}
const getStatusText = (status) => {
const statusMap = {
'prepare': '筹备中',
'starting': '进行中',
'finish': '已结束'
}
return statusMap[status] || status
}
// 初始化数据
const initializeData = async () => {
loading.value = true
error.value = ''
try {
// 并行获取所有数据
await Promise.all([
fetchAuthors(),
fetchCompetitions(),
fetchTerrains(),
fetchPopularTags(),
fetchDemands()
])
} catch (err) {
console.error('初始化数据失败:', err)
error.value = '数据加载失败,请刷新页面重试'
} finally {
loading.value = false
}
}
onMounted(() => {
initializeData()
})
</script>
<style scoped>
.home-page {
padding: 16px;
max-width: 1400px;
margin: 0 auto;
}
.page-header {
text-align: center;
margin-bottom: 30px;
}
.page-header h1 {
font-size: 2.2rem;
color: #1a237e;
margin: 0 0 10px 0;
font-weight: 600;
}
.header-subtitle {
color: #666;
font-size: 1.1rem;
margin-bottom: 20px;
}
/* 搜索区域 */
.search-section {
margin-bottom: 40px;
}
.search-container {
max-width: 800px;
margin: 0 auto;
}
.search-box {
display: flex;
margin-bottom: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-radius: 8px;
overflow: hidden;
}
.search-input {
flex: 1;
padding: 15px 20px;
border: none;
font-size: 16px;
outline: none;
}
.search-btn {
padding: 15px 25px;
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
color: white;
border: none;
cursor: pointer;
transition: all 0.3s ease;
}
.search-btn:hover {
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
}
.search-tags {
text-align: center;
}
.tag-label {
color: #666;
margin-right: 15px;
font-weight: 500;
}
.search-tag {
display: inline-block;
padding: 6px 12px;
margin: 0 5px;
background: #e8eaf6;
color: #1a237e;
border-radius: 16px;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
}
.search-tag:hover {
background: #71eaeb;
color: white;
transform: translateY(-1px);
}
/* 内容网格布局 */
.content-grid {
display: grid;
grid-template-columns: 2fr 1fr;
gap: 30px;
}
.main-content {
display: flex;
flex-direction: column;
gap: 30px;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 20px;
}
/* 通用卡片样式 */
.section-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 2px solid #f0f0f0;
}
.section-header h2,
.section-header h3 {
display: flex;
align-items: center;
gap: 10px;
color: #1a237e;
margin: 0;
font-weight: 600;
}
.section-header h2 {
font-size: 1.3rem;
}
.section-header h3 {
font-size: 1.1rem;
}
.section-header i {
color: #71eaeb;
}
.more-link {
color: #71eaeb;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: color 0.3s ease;
}
.more-link:hover {
color: #416bdf;
}
/* 作者推荐 */
.authors-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.author-card {
padding: 20px;
border-radius: 8px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.author-card:hover {
background: #f0f7ff;
transform: translateY(-2px);
border-color: #71eaeb;
}
.author-info h4 {
margin: 0 0 10px 0;
color: #1a237e;
font-size: 1.2rem;
font-weight: 600;
}
.author-info p {
margin: 0 0 12px 0;
color: #666;
font-size: 1rem;
}
.author-stats {
display: flex;
flex-direction: column;
gap: 8px;
font-size: 0.9rem;
color: #888;
}
.author-stats span {
display: flex;
align-items: center;
gap: 8px;
}
.author-stats i {
color: #71eaeb;
width: 16px;
}
/* 在线工具 */
.tools-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
}
.tool-card {
padding: 20px;
border: 2px solid #f0f0f0;
border-radius: 8px;
text-align: center;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.tool-card:hover {
border-color: #71eaeb;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.tool-icon {
width: 50px;
height: 50px;
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 15px;
}
.tool-icon i {
font-size: 24px;
color: white;
}
.tool-card h4 {
margin: 0 0 10px 0;
color: #1a237e;
font-size: 1.1rem;
}
.tool-card p {
color: #666;
font-size: 0.9rem;
margin-bottom: 15px;
}
.tool-badge {
position: absolute;
top: 10px;
right: 10px;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
color: white;
}
.tool-badge.mod {
background: #6c5ce7;
}
.tool-badge.map {
background: #0984e3;
}
/* 地形与纹理 */
.terrain-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 12px;
}
.terrain-card {
aspect-ratio: 1;
border-radius: 6px;
overflow: hidden;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
}
.terrain-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
}
.terrain-image {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
.terrain-image:hover {
transform: scale(1.03);
}
/* 赛事信息 */
.competitions-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.competition-item {
padding: 15px;
border-radius: 8px;
background: #f8f9fa;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
}
.competition-item:hover {
background: #f0f7ff;
transform: translateX(4px);
}
.competition-status {
position: absolute;
top: 10px;
right: 10px;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
color: white;
}
.competition-status.prepare {
background: #e6a23c;
}
.competition-status.starting {
background: #67c23a;
}
.competition-status.finish {
background: #909399;
}
.competition-item h4 {
margin: 0 0 8px 0;
color: #1a237e;
font-size: 1rem;
}
.competition-time {
display: flex;
align-items: center;
gap: 5px;
color: #888;
font-size: 0.85rem;
margin-bottom: 5px;
}
.competition-time i {
color: #71eaeb;
}
.competition-item p {
color: #666;
font-size: 0.85rem;
margin: 0;
}
/* 办事大厅 */
.demands-list {
display: flex;
flex-direction: column;
gap: 18px;
}
.demand-item {
padding: 16px 16px 12px 16px;
border-radius: 10px;
background: linear-gradient(90deg, #e3f2fd 0%, #f8f9fa 100%);
border-left: 5px solid #71eaeb;
box-shadow: 0 2px 8px rgba(113,234,235,0.08);
transition: box-shadow 0.2s, background 0.2s;
position: relative;
}
.demand-item:hover {
background: #f0f7ff;
box-shadow: 0 4px 16px rgba(113,234,235,0.15);
}
.demand-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 6px;
}
.demand-title {
margin: 0;
color: #1a237e;
font-size: 1.05rem;
font-weight: 600;
flex: 1;
word-break: break-all;
}
.demand-reward {
border-radius: 12px;
padding: 2px 10px;
font-size: 0.85rem;
font-weight: 500;
margin-left: 10px;
white-space: nowrap;
}
.demand-reward.has-reward {
background: #fff7e6;
color: #d46b08;
border: 1px solid #ffd591;
}
.demand-reward.no-reward {
background: #f5f5f5;
color: #8c8c8c;
border: 1px solid #d9d9d9;
}
.demand-meta {
display: flex;
justify-content: space-between;
color: #888;
font-size: 0.85rem;
margin-bottom: 4px;
}
.demand-requester {
font-weight: 500;
}
.demand-date {
font-style: italic;
}
.demand-bounty {
color: #1a237e;
font-size: 1rem;
margin-bottom: 6px;
font-weight: 500;
word-break: break-all;
}
.demands-toggle-btn-wrapper {
display: flex;
justify-content: center;
margin-top: 10px;
}
.demands-toggle-btn {
background: linear-gradient(90deg, #71eaeb 0%, #416bdf 100%);
color: #fff;
border: none;
border-radius: 18px;
padding: 6px 28px;
font-size: 15px;
font-weight: 500;
cursor: pointer;
transition: background 0.2s, color 0.2s;
}
.demands-toggle-btn:hover {
background: linear-gradient(90deg, #416bdf 0%, #71eaeb 100%);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.content-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.authors-grid {
grid-template-columns: 1fr;
}
.tools-grid {
grid-template-columns: repeat(2, 1fr);
}
.terrain-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.home-page {
padding: 12px;
}
.page-header h1 {
font-size: 1.8rem;
}
.search-input {
padding: 12px 15px;
font-size: 14px;
}
.search-btn {
padding: 12px 20px;
}
.section-card {
padding: 18px;
}
.tools-grid {
grid-template-columns: 1fr;
}
.terrain-grid {
grid-template-columns: repeat(2, 1fr);
}
.search-tags {
text-align: left;
}
.search-tag {
margin: 2px;
font-size: 13px;
}
}
/* 加载和错误状态样式 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #71eaeb;
border-radius: 50%;
animation: spin 1s linear infinite;
margin-bottom: 20px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-container p {
color: #666;
font-size: 1.1rem;
margin: 0;
}
.error-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 20px;
text-align: center;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
}
.error-icon {
font-size: 3rem;
margin-bottom: 20px;
}
.error-container p {
color: #f56c6c;
font-size: 1.1rem;
margin: 0 0 20px 0;
max-width: 400px;
}
.retry-btn {
padding: 12px 24px;
background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
color: white;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
}
.retry-btn:hover {
background: linear-gradient(135deg, #416bdf 0%, #71eaeb 100%);
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
</style>