1073 lines
24 KiB
Vue
1073 lines
24 KiB
Vue
<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> |