提交
This commit is contained in:
		
							parent
							
								
									56d8d7a345
								
							
						
					
					
						commit
						062550d126
					
				
							
								
								
									
										13
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="UTF-8">
 | 
			
		||||
    <link rel="icon" href="/favicon.ico">
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | 
			
		||||
    <title>Vite App</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="app"></div>
 | 
			
		||||
    <script type="module" src="/src/main.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										8
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								jsconfig.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
{
 | 
			
		||||
  "compilerOptions": {
 | 
			
		||||
    "paths": {
 | 
			
		||||
      "@/*": ["./src/*"]
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "exclude": ["node_modules", "dist"]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2887
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2887
									
								
								package-lock.json
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										21
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "untitled2",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "axios": "^1.9.0",
 | 
			
		||||
    "vue": "^3.5.13",
 | 
			
		||||
    "vue-router": "^4.5.1"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@vitejs/plugin-vue": "^5.2.3",
 | 
			
		||||
    "vite": "^6.2.4",
 | 
			
		||||
    "vite-plugin-vue-devtools": "^7.7.2"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9231
									
								
								public/Weapon.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9231
									
								
								public/Weapon.xml
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								public/favicon.ico
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 4.2 KiB  | 
							
								
								
									
										128
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,128 @@
 | 
			
		||||
<script setup>
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="app">
 | 
			
		||||
    <nav class="navbar">
 | 
			
		||||
      <div class="nav-container">
 | 
			
		||||
        <div class="nav-left">
 | 
			
		||||
          <div class="nav-brand">红色警戒3数据分析中心</div>
 | 
			
		||||
          <router-link to="/" class="nav-link">热门下载地图</router-link>
 | 
			
		||||
          <router-link to="/weekly" class="nav-link">每周推荐</router-link>
 | 
			
		||||
          <router-link to="/weapon-match" class="nav-link">Weapon 匹配</router-link>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </nav>
 | 
			
		||||
    <main class="main-content">
 | 
			
		||||
      <router-view></router-view>
 | 
			
		||||
    </main>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style>
 | 
			
		||||
* {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
 | 
			
		||||
  background-color: #f5f7fa;
 | 
			
		||||
  color: #2c3e50;
 | 
			
		||||
  line-height: 1.6;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app {
 | 
			
		||||
  min-height: 100vh;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-direction: column;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.navbar {
 | 
			
		||||
  background: linear-gradient(135deg, #71eaeb 0%, #416bdf 100%);
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  position: fixed;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  right: 0;
 | 
			
		||||
  z-index: 1000;
 | 
			
		||||
  box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-container {
 | 
			
		||||
  max-width: 1200px;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
  padding: 0 20px;
 | 
			
		||||
  height: 60px;
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-left {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-brand {
 | 
			
		||||
  color: white;
 | 
			
		||||
  font-size: 1.5rem;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-link {
 | 
			
		||||
  color: rgba(255, 255, 255, 0.9);
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  padding: 8px 16px;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  transition: all 0.3s ease;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-link:hover {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.1);
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.nav-link.router-link-active {
 | 
			
		||||
  background-color: rgba(255, 255, 255, 0.2);
 | 
			
		||||
  color: white;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main-content {
 | 
			
		||||
  margin-top: 60px;
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  flex: 1;
 | 
			
		||||
  max-width: 1200px;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  margin-left: auto;
 | 
			
		||||
  margin-right: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 响应式设计 */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
  .nav-container {
 | 
			
		||||
    padding: 0 15px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav-left {
 | 
			
		||||
    gap: 15px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav-brand {
 | 
			
		||||
    font-size: 1.2rem;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .nav-link {
 | 
			
		||||
    padding: 6px 12px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .main-content {
 | 
			
		||||
    padding: 15px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										65
									
								
								src/api/maps.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/api/maps.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
import axios from 'axios'
 | 
			
		||||
 | 
			
		||||
const API_BASE_URL = 'https://ra3.z31.xyz/v1'
 | 
			
		||||
 | 
			
		||||
// 获取地图列表
 | 
			
		||||
export const getMaps = async (params = {}) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await axios.get(`${API_BASE_URL}/maps/`, {
 | 
			
		||||
      params: {
 | 
			
		||||
        p: params.page || 1,
 | 
			
		||||
        search: params.search || '',
 | 
			
		||||
        player_count: params.player_count || '',
 | 
			
		||||
        tags: params.tags || '',
 | 
			
		||||
        ordering: params.ordering || '-download_count'
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return response.data
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取地图列表失败:', error)
 | 
			
		||||
    throw error
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取每周推荐地图
 | 
			
		||||
export const getWeeklyTopMaps = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await axios.get('https://ra3.z31.xyz/v1/maps/', {
 | 
			
		||||
      params: {
 | 
			
		||||
        ordering: '-download_count',
 | 
			
		||||
        format: 'json',
 | 
			
		||||
        p: 1
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return response.data.results.slice(0, 10)
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取每周推荐地图失败:', error)
 | 
			
		||||
    throw error
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取地图详情
 | 
			
		||||
export const getMapDetail = async (id) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await axios.get(`${API_BASE_URL}/maps/${id}/`)
 | 
			
		||||
    return response.data
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取地图详情失败:', error)
 | 
			
		||||
    throw error
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 获取所有标签
 | 
			
		||||
export const getAllTags = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    const response = await axios.get(`${API_BASE_URL}/maps/`)
 | 
			
		||||
    const tags = new Set()
 | 
			
		||||
    response.data.results.forEach(map => {
 | 
			
		||||
      map.tags.forEach(tag => tags.add(tag))
 | 
			
		||||
    })
 | 
			
		||||
    return Array.from(tags)
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取标签列表失败:', error)
 | 
			
		||||
    throw error
 | 
			
		||||
  }
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										176
									
								
								src/assets/styles/Maps.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								src/assets/styles/Maps.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,176 @@
 | 
			
		||||
.maps {
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.search-box {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.search-box input {
 | 
			
		||||
  padding: 6px 12px;
 | 
			
		||||
  border: 1px solid #e0e0e0;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  width: 200px;
 | 
			
		||||
  transition: all 0.3s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.search-box input:focus {
 | 
			
		||||
  outline: none;
 | 
			
		||||
  border-color: #1a237e;
 | 
			
		||||
  box-shadow: 0 0 0 2px rgba(26, 35, 126, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filters {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  gap: 16px;
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.filter-select {
 | 
			
		||||
  padding: 6px 12px;
 | 
			
		||||
  border: 1px solid #e0e0e0;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  background: #fff;
 | 
			
		||||
  min-width: 100px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 分页样式 */
 | 
			
		||||
.pagination {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 16px;
 | 
			
		||||
  margin-top: 20px;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  padding: 20px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-numbers {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-btn,
 | 
			
		||||
.page-number {
 | 
			
		||||
  padding: 8px 16px;
 | 
			
		||||
  border: 1px solid #e0e0e0;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  background: white;
 | 
			
		||||
  color: #1a237e;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: all 0.2s ease;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  min-width: 40px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-btn:hover:not(:disabled),
 | 
			
		||||
.page-number:hover:not(.active) {
 | 
			
		||||
  background: #f5f7fa;
 | 
			
		||||
  border-color: #1a237e;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-btn:disabled {
 | 
			
		||||
  opacity: 0.5;
 | 
			
		||||
  cursor: not-allowed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-number.active {
 | 
			
		||||
  background: #1a237e;
 | 
			
		||||
  color: white;
 | 
			
		||||
  border-color: #1a237e;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-ellipsis {
 | 
			
		||||
  color: #666;
 | 
			
		||||
  padding: 0 8px;
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-jump {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  gap: 8px;
 | 
			
		||||
  color: #666;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  justify-content: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-jump input {
 | 
			
		||||
  width: 50px;
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  border: 1px solid #e0e0e0;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-jump input:focus {
 | 
			
		||||
  outline: none;
 | 
			
		||||
  border-color: #1a237e;
 | 
			
		||||
  box-shadow: 0 0 0 2px rgba(26, 35, 126, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-btn {
 | 
			
		||||
  padding: 4px 12px;
 | 
			
		||||
  border: 1px solid #1a237e;
 | 
			
		||||
  border-radius: 4px;
 | 
			
		||||
  background: #1a237e;
 | 
			
		||||
  color: white;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: all 0.2s ease;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-btn:hover:not(:disabled) {
 | 
			
		||||
  background: #283593;
 | 
			
		||||
  border-color: #283593;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.jump-btn:disabled {
 | 
			
		||||
  background: #e0e0e0;
 | 
			
		||||
  border-color: #e0e0e0;
 | 
			
		||||
  color: #999;
 | 
			
		||||
  cursor: not-allowed;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
  .search-box input {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .pagination {
 | 
			
		||||
    gap: 12px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .page-btn,
 | 
			
		||||
  .page-number {
 | 
			
		||||
    padding: 6px 12px;
 | 
			
		||||
    font-size: 13px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .page-jump {
 | 
			
		||||
    font-size: 13px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .page-jump input {
 | 
			
		||||
    width: 40px;
 | 
			
		||||
    padding: 3px 6px;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  .jump-btn {
 | 
			
		||||
    padding: 3px 10px;
 | 
			
		||||
    font-size: 13px;
 | 
			
		||||
  }
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										13
									
								
								src/assets/styles/WeeklyRecommend.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/assets/styles/WeeklyRecommend.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
.weekly-recommend {
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.header-subtitle {
 | 
			
		||||
  color: #666;
 | 
			
		||||
  font-size: 0.9rem;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-name {
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
  color: #1a237e;
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										84
									
								
								src/assets/styles/common.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/assets/styles/common.css
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
/* 表格通用样式 */
 | 
			
		||||
.table-container {
 | 
			
		||||
  background: white;
 | 
			
		||||
  border-radius: 12px;
 | 
			
		||||
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.maps-table {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border-collapse: collapse;
 | 
			
		||||
  min-width: 1000px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.maps-table th,
 | 
			
		||||
.maps-table td {
 | 
			
		||||
  padding: 16px;
 | 
			
		||||
  text-align: left;
 | 
			
		||||
  border-bottom: 1px solid #f0f0f0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.maps-table th {
 | 
			
		||||
  background-color: #f8f9fa;
 | 
			
		||||
  font-weight: 600;
 | 
			
		||||
  color: #1a237e;
 | 
			
		||||
  white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-row {
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: all 0.2s ease;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.table-row:hover {
 | 
			
		||||
  background-color: #f8f9fa;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 预览图样式 */
 | 
			
		||||
.preview-cell {
 | 
			
		||||
  width: 100px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.preview-cell img {
 | 
			
		||||
  width: 80px;
 | 
			
		||||
  height: 80px;
 | 
			
		||||
  object-fit: cover;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 标签样式 */
 | 
			
		||||
.tags {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  gap: 4px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tag {
 | 
			
		||||
  background: #e8eaf6;
 | 
			
		||||
  color: #1a237e;
 | 
			
		||||
  padding: 4px 8px;
 | 
			
		||||
  border-radius: 12px;
 | 
			
		||||
  font-size: 12px;
 | 
			
		||||
  font-weight: 500;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 页面标题样式 */
 | 
			
		||||
.page-header {
 | 
			
		||||
  margin-bottom: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.page-header h1 {
 | 
			
		||||
  font-size: 1.8rem;
 | 
			
		||||
  color: #1a237e;
 | 
			
		||||
  margin: 0 0 8px 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 响应式设计 */
 | 
			
		||||
@media (max-width: 768px) {
 | 
			
		||||
  .table-container {
 | 
			
		||||
    margin: 0 -20px;
 | 
			
		||||
    border-radius: 0;
 | 
			
		||||
  }
 | 
			
		||||
} 
 | 
			
		||||
							
								
								
									
										52
									
								
								src/components/FileUploader.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/components/FileUploader.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,52 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <div>
 | 
			
		||||
      <label>
 | 
			
		||||
        上传 Weapon.xml:
 | 
			
		||||
        <input type="file" @change="onWeaponFileChange" />
 | 
			
		||||
      </label>
 | 
			
		||||
      <br />
 | 
			
		||||
      <label>
 | 
			
		||||
        上传单位 XML(如 AlliedAntiInfantryInfantry.xml):
 | 
			
		||||
        <input type="file" @change="onUnitFileChange" />
 | 
			
		||||
      </label>
 | 
			
		||||
    </div>
 | 
			
		||||
  </template>
 | 
			
		||||
  
 | 
			
		||||
  <script setup>
 | 
			
		||||
  import { ref } from 'vue'
 | 
			
		||||
  
 | 
			
		||||
  const emit = defineEmits(['updateWeaponTemplates', 'updateUnitDoc'])
 | 
			
		||||
  
 | 
			
		||||
  const onWeaponFileChange = (e) => {
 | 
			
		||||
    const file = e.target.files[0]
 | 
			
		||||
    if (!file) return
 | 
			
		||||
  
 | 
			
		||||
    const reader = new FileReader()
 | 
			
		||||
    reader.onload = (event) => {
 | 
			
		||||
      const parser = new DOMParser()
 | 
			
		||||
      const xml = parser.parseFromString(event.target.result, 'text/xml')
 | 
			
		||||
      const templates = xml.querySelectorAll('WeaponTemplate')
 | 
			
		||||
      const ids = new Set()
 | 
			
		||||
      templates.forEach((t) => {
 | 
			
		||||
        const id = t.getAttribute('id')
 | 
			
		||||
        if (id) ids.add(id)
 | 
			
		||||
      })
 | 
			
		||||
      emit('updateWeaponTemplates', ids)
 | 
			
		||||
    }
 | 
			
		||||
    reader.readAsText(file)
 | 
			
		||||
  }
 | 
			
		||||
  
 | 
			
		||||
  const onUnitFileChange = (e) => {
 | 
			
		||||
    const file = e.target.files[0]
 | 
			
		||||
    if (!file) return
 | 
			
		||||
  
 | 
			
		||||
    const reader = new FileReader()
 | 
			
		||||
    reader.onload = (event) => {
 | 
			
		||||
      const parser = new DOMParser()
 | 
			
		||||
      const xml = parser.parseFromString(event.target.result, 'text/xml')
 | 
			
		||||
      emit('updateUnitDoc', xml)
 | 
			
		||||
    }
 | 
			
		||||
    reader.readAsText(file)
 | 
			
		||||
  }
 | 
			
		||||
  </script>
 | 
			
		||||
  
 | 
			
		||||
							
								
								
									
										26
									
								
								src/components/MatchResultTable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/components/MatchResultTable.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,26 @@
 | 
			
		||||
<template>
 | 
			
		||||
    <table>
 | 
			
		||||
      <thead>
 | 
			
		||||
        <tr>
 | 
			
		||||
          <th>GameObject id</th>
 | 
			
		||||
          <th>匹配到的 WeaponTemplate</th>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </thead>
 | 
			
		||||
      <tbody>
 | 
			
		||||
        <tr v-for="(result, index) in matchResults" :key="index">
 | 
			
		||||
          <td>{{ result.goId }}</td>
 | 
			
		||||
          <td>{{ result.templates.length > 0 ? result.templates.join(', ') : 'None' }}</td>
 | 
			
		||||
        </tr>
 | 
			
		||||
      </tbody>
 | 
			
		||||
    </table>
 | 
			
		||||
  </template>
 | 
			
		||||
  
 | 
			
		||||
  <script setup>
 | 
			
		||||
  defineProps({
 | 
			
		||||
    matchResults: {
 | 
			
		||||
      type: Array,
 | 
			
		||||
      required: true
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  </script>
 | 
			
		||||
  
 | 
			
		||||
							
								
								
									
										11
									
								
								src/components/index.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/components/index.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
							
								
								
									
										8
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/main.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
 | 
			
		||||
import { createApp } from 'vue'
 | 
			
		||||
import App from './App.vue'
 | 
			
		||||
import router from './router'
 | 
			
		||||
 | 
			
		||||
const app = createApp(App)
 | 
			
		||||
app.use(router)
 | 
			
		||||
app.mount('#app')
 | 
			
		||||
							
								
								
									
										31
									
								
								src/router/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/router/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
import { createRouter, createWebHistory } from 'vue-router'
 | 
			
		||||
 | 
			
		||||
const routes = [
 | 
			
		||||
  {
 | 
			
		||||
    path: '/',
 | 
			
		||||
    name: 'Maps',
 | 
			
		||||
    component: () => import('../views/Maps.vue')
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/weekly',
 | 
			
		||||
    name: 'WeeklyRecommend',
 | 
			
		||||
    component: () => import('../views/WeeklyRecommend.vue')
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/map/:id',
 | 
			
		||||
    name: 'MapDetail',
 | 
			
		||||
    component: () => import('../views/MapDetail.vue')
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    path: '/weapon-match',
 | 
			
		||||
    name: 'WeaponMatch',
 | 
			
		||||
    component: () => import('../views/WeaponMatch.vue') 
 | 
			
		||||
  }
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
const router = createRouter({
 | 
			
		||||
  history: createWebHistory(),
 | 
			
		||||
  routes
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export default router 
 | 
			
		||||
							
								
								
									
										178
									
								
								src/views/MapDetail.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/views/MapDetail.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,178 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="map-detail" v-if="map">
 | 
			
		||||
    <div class="back-button">
 | 
			
		||||
      <button @click="goBack" class="back-btn">
 | 
			
		||||
        <span class="back-icon">←</span> 返回列表
 | 
			
		||||
      </button>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="map-header">
 | 
			
		||||
      <h1>{{ map.chinese_name }}</h1>
 | 
			
		||||
      <p class="author">作者: {{ map.user }}</p>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <div class="map-content">
 | 
			
		||||
      <div class="map-image">
 | 
			
		||||
        <img :src="map.img_file" :alt="map.chinese_name">
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <div class="map-info">
 | 
			
		||||
        <div class="info-item">
 | 
			
		||||
          <h3>基本信息</h3>
 | 
			
		||||
          <p>下载次数: {{ map.download_count }}</p>
 | 
			
		||||
          <p>收藏次数: {{ map.favourite_count }}</p>
 | 
			
		||||
          <p>玩家数量: {{ map.player_count }}</p>
 | 
			
		||||
          <p>创建时间: {{ formatDate(map.create_time) }}</p>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="tags">
 | 
			
		||||
          <h3>标签</h3>
 | 
			
		||||
          <div class="tag-list">
 | 
			
		||||
            <span v-for="tag in map.tags" :key="tag" class="tag">{{ tag }}</span>
 | 
			
		||||
          </div>
 | 
			
		||||
        </div>
 | 
			
		||||
        
 | 
			
		||||
        <div class="actions">
 | 
			
		||||
          <a :href="map.zip_file" class="download-btn" download>下载地图</a>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, onMounted } from 'vue'
 | 
			
		||||
import { useRoute, useRouter } from 'vue-router'
 | 
			
		||||
import { getMapDetail } from '../api/maps'
 | 
			
		||||
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const map = ref(null)
 | 
			
		||||
 | 
			
		||||
const fetchMapDetail = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    map.value = await getMapDetail(route.params.id)
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取地图详情失败:', error)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const goBack = () => {
 | 
			
		||||
  router.back()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const formatDate = (dateString) => {
 | 
			
		||||
  return new Date(dateString).toLocaleDateString('zh-CN')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  fetchMapDetail()
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.map-detail {
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  max-width: 1200px;
 | 
			
		||||
  margin: 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.back-button {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.back-btn {
 | 
			
		||||
  display: inline-flex;
 | 
			
		||||
  align-items: center;
 | 
			
		||||
  padding: 8px 16px;
 | 
			
		||||
  background: #f8f9fa;
 | 
			
		||||
  border: 1px solid #e0e0e0;
 | 
			
		||||
  border-radius: 6px;
 | 
			
		||||
  color: #333;
 | 
			
		||||
  cursor: pointer;
 | 
			
		||||
  transition: all 0.2s ease;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.back-btn:hover {
 | 
			
		||||
  background: #e9ecef;
 | 
			
		||||
  border-color: #d0d0d0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.back-icon {
 | 
			
		||||
  margin-right: 6px;
 | 
			
		||||
  font-size: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-header {
 | 
			
		||||
  margin-bottom: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-header h1 {
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  color: #333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.author {
 | 
			
		||||
  color: #666;
 | 
			
		||||
  margin-top: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-content {
 | 
			
		||||
  display: grid;
 | 
			
		||||
  grid-template-columns: 1fr 1fr;
 | 
			
		||||
  gap: 30px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-image img {
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
  box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.map-info {
 | 
			
		||||
  padding: 20px;
 | 
			
		||||
  background: #f8f9fa;
 | 
			
		||||
  border-radius: 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.info-item {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.info-item h3 {
 | 
			
		||||
  margin-bottom: 10px;
 | 
			
		||||
  color: #333;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tags {
 | 
			
		||||
  margin-bottom: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tag-list {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  gap: 10px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tag {
 | 
			
		||||
  background: #e9ecef;
 | 
			
		||||
  padding: 5px 10px;
 | 
			
		||||
  border-radius: 15px;
 | 
			
		||||
  font-size: 14px;
 | 
			
		||||
  color: #495057;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.download-btn {
 | 
			
		||||
  display: inline-block;
 | 
			
		||||
  padding: 10px 20px;
 | 
			
		||||
  background: #007bff;
 | 
			
		||||
  color: white;
 | 
			
		||||
  text-decoration: none;
 | 
			
		||||
  border-radius: 5px;
 | 
			
		||||
  transition: background 0.2s;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.download-btn:hover {
 | 
			
		||||
  background: #0056b3;
 | 
			
		||||
}
 | 
			
		||||
</style> 
 | 
			
		||||
							
								
								
									
										257
									
								
								src/views/Maps.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								src/views/Maps.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,257 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="maps">
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
      <h1>热门下载地图</h1>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="filters">
 | 
			
		||||
      <div class="search-box">
 | 
			
		||||
        <input
 | 
			
		||||
          type="text"
 | 
			
		||||
          placeholder="搜索地图..."
 | 
			
		||||
          v-model="searchValue"
 | 
			
		||||
          @input="handleSearchInput"
 | 
			
		||||
          @blur="handleSearchBlur"
 | 
			
		||||
        >
 | 
			
		||||
      </div>
 | 
			
		||||
      <select v-model="playerCountFilter" class="filter-select" @change="handleFilterChange">
 | 
			
		||||
        <option value="">玩家数</option>
 | 
			
		||||
        <option v-for="n in [2,3,4,5,6,7,8]" :key="n" :value="n">{{ n }}人</option>
 | 
			
		||||
      </select>
 | 
			
		||||
      <select v-model="tagFilter" class="filter-select" @change="handleFilterChange">
 | 
			
		||||
        <option value="">分类</option>
 | 
			
		||||
        <option v-for="tag in tagOptions" :key="tag" :value="tag">{{ tag }}</option>
 | 
			
		||||
      </select>
 | 
			
		||||
      <select v-model="selectedOrder" class="filter-select" @change="handleFilterChange">
 | 
			
		||||
        <option v-for="opt in orderOptions" :key="opt.value" :value="opt.value">{{ opt.label }}</option>
 | 
			
		||||
      </select>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="table-container">
 | 
			
		||||
      <table class="maps-table">
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>预览图</th>
 | 
			
		||||
            <th>地图名称</th>
 | 
			
		||||
            <th>作者</th>
 | 
			
		||||
            <th>下载次数</th>
 | 
			
		||||
            <th>收藏次数</th>
 | 
			
		||||
            <th>玩家数量</th>
 | 
			
		||||
            <th>创建时间</th>
 | 
			
		||||
            <th>标签</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr v-for="map in maps" :key="map.id" @click="goToMapDetail(map.id)" class="table-row">
 | 
			
		||||
            <td class="preview-cell">
 | 
			
		||||
              <img :src="map.thumbnail" :alt="map.chinese_name">
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="map-name">{{ map.chinese_name }}</td>
 | 
			
		||||
            <td>{{ map.user }}</td>
 | 
			
		||||
            <td>{{ map.download_count }}</td>
 | 
			
		||||
            <td>{{ map.favourite_count }}</td>
 | 
			
		||||
            <td>{{ map.player_count }}</td>
 | 
			
		||||
            <td>{{ formatDate(map.create_time) }}</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="tags">
 | 
			
		||||
                <span v-for="tag in map.tags" :key="tag" class="tag">{{ tag }}</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
    
 | 
			
		||||
    <!-- 分页控件 -->
 | 
			
		||||
    <div class="pagination">
 | 
			
		||||
      <button 
 | 
			
		||||
        class="page-btn" 
 | 
			
		||||
        :disabled="currentPage === 1"
 | 
			
		||||
        @click="changePage(currentPage - 1)"
 | 
			
		||||
      >
 | 
			
		||||
        上一页
 | 
			
		||||
      </button>
 | 
			
		||||
      
 | 
			
		||||
      <div class="page-numbers">
 | 
			
		||||
        <template v-for="(page, index) in displayedPages" :key="index">
 | 
			
		||||
          <button 
 | 
			
		||||
            v-if="typeof page === 'number'"
 | 
			
		||||
            class="page-number"
 | 
			
		||||
            :class="{ active: page === currentPage }"
 | 
			
		||||
            @click="changePage(page)"
 | 
			
		||||
          >
 | 
			
		||||
            {{ page }}
 | 
			
		||||
          </button>
 | 
			
		||||
          <span v-else class="page-ellipsis">{{ page }}</span>
 | 
			
		||||
        </template>
 | 
			
		||||
      </div>
 | 
			
		||||
      
 | 
			
		||||
      <button 
 | 
			
		||||
        class="page-btn" 
 | 
			
		||||
        :disabled="!hasNextPage"
 | 
			
		||||
        @click="changePage(currentPage + 1)"
 | 
			
		||||
      >
 | 
			
		||||
        下一页
 | 
			
		||||
      </button>
 | 
			
		||||
 | 
			
		||||
      <div class="page-jump">
 | 
			
		||||
        <input 
 | 
			
		||||
          type="number" 
 | 
			
		||||
          v-model="jumpPage" 
 | 
			
		||||
          :min="1" 
 | 
			
		||||
          :max="totalPages"
 | 
			
		||||
          class="jump-input"
 | 
			
		||||
          @keyup.enter="handleJumpPage"
 | 
			
		||||
        >
 | 
			
		||||
        <button class="jump-btn" @click="handleJumpPage">跳转</button>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, computed, onMounted, watch } from 'vue'
 | 
			
		||||
import { useRouter, useRoute } from 'vue-router'
 | 
			
		||||
import { getMaps, getAllTags } from '../api/maps'
 | 
			
		||||
import '../assets/styles/common.css'
 | 
			
		||||
import '../assets/styles/Maps.css'
 | 
			
		||||
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const route = useRoute()
 | 
			
		||||
const maps = ref([])
 | 
			
		||||
const currentPage = ref(1)
 | 
			
		||||
const totalPages = ref(1)
 | 
			
		||||
const hasNextPage = ref(false)
 | 
			
		||||
const jumpPage = ref('')
 | 
			
		||||
 | 
			
		||||
// 排序和筛选相关
 | 
			
		||||
const orderOptions = [
 | 
			
		||||
  { label: '下载量降序', value: '-download_count' },
 | 
			
		||||
  { label: '下载量升序', value: 'download_count' },
 | 
			
		||||
  { label: '收藏数降序', value: '-favourite_count' },
 | 
			
		||||
  { label: '收藏数升序', value: 'favourite_count' },
 | 
			
		||||
  { label: '创建时间降序', value: '-create_time' },
 | 
			
		||||
  { label: '创建时间升序', value: 'create_time' }
 | 
			
		||||
]
 | 
			
		||||
const selectedOrder = ref('-download_count')
 | 
			
		||||
const searchValue = ref('')
 | 
			
		||||
const playerCountFilter = ref('')
 | 
			
		||||
const tagFilter = ref('')
 | 
			
		||||
const tagOptions = ref([])
 | 
			
		||||
 | 
			
		||||
// 计算显示的页码
 | 
			
		||||
const displayedPages = computed(() => {
 | 
			
		||||
  const pages = []
 | 
			
		||||
  const total = totalPages.value
 | 
			
		||||
  const current = currentPage.value
 | 
			
		||||
 | 
			
		||||
  if (total <= 5) {
 | 
			
		||||
    for (let i = 1; i <= total; i++) {
 | 
			
		||||
      pages.push(i)
 | 
			
		||||
    }
 | 
			
		||||
  } else if (current <= 3) {
 | 
			
		||||
    pages.push(1, 2, 3, 4, '...', total)
 | 
			
		||||
  } else if (current >= total - 2) {
 | 
			
		||||
    pages.push(1, '...')
 | 
			
		||||
    for (let i = total - 2; i <= total; i++) {
 | 
			
		||||
      if (i > 1) pages.push(i)
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    pages.push(1, '...', current - 1, current, current + 1, '...', total)
 | 
			
		||||
  }
 | 
			
		||||
  return pages
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
// 获取地图数据
 | 
			
		||||
const fetchMaps = async (page = 1) => {
 | 
			
		||||
  try {
 | 
			
		||||
    const params = {
 | 
			
		||||
      page,
 | 
			
		||||
      search: searchValue.value,
 | 
			
		||||
      player_count: playerCountFilter.value,
 | 
			
		||||
      tags: tagFilter.value,
 | 
			
		||||
      ordering: selectedOrder.value
 | 
			
		||||
    }
 | 
			
		||||
    console.log('正在获取地图数据,参数:', params)
 | 
			
		||||
    const data = await getMaps(params)
 | 
			
		||||
    maps.value = data.results
 | 
			
		||||
    hasNextPage.value = !!data.next
 | 
			
		||||
    totalPages.value = Math.ceil(data.count / 20)
 | 
			
		||||
    currentPage.value = page
 | 
			
		||||
    console.log('分页状态:', {
 | 
			
		||||
      currentPage: currentPage.value,
 | 
			
		||||
      totalPages: totalPages.value,
 | 
			
		||||
      hasNextPage: hasNextPage.value,
 | 
			
		||||
      resultsCount: data.results.length
 | 
			
		||||
    })
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取地图列表失败:', error)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理搜索输入
 | 
			
		||||
const handleSearchInput = () => {
 | 
			
		||||
  if (!searchValue.value) {
 | 
			
		||||
    fetchMaps(1)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理搜索框失去焦点
 | 
			
		||||
const handleSearchBlur = () => {
 | 
			
		||||
  if (searchValue.value) {
 | 
			
		||||
    fetchMaps(1)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理筛选条件变化
 | 
			
		||||
const handleFilterChange = () => {
 | 
			
		||||
  console.log('筛选条件变化:', {
 | 
			
		||||
    playerCount: playerCountFilter.value,
 | 
			
		||||
    tag: tagFilter.value,
 | 
			
		||||
    order: selectedOrder.value
 | 
			
		||||
  })
 | 
			
		||||
  fetchMaps(1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 处理页码跳转
 | 
			
		||||
const handleJumpPage = () => {
 | 
			
		||||
  const page = parseInt(jumpPage.value)
 | 
			
		||||
  if (page && page >= 1 && page <= totalPages.value) {
 | 
			
		||||
    changePage(page)
 | 
			
		||||
  }
 | 
			
		||||
  jumpPage.value = ''
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 切换页码
 | 
			
		||||
const changePage = (page) => {
 | 
			
		||||
  console.log('尝试切换页码:', page, '当前页码:', currentPage.value, '总页数:', totalPages.value)
 | 
			
		||||
  if (page < 1 || page > totalPages.value) {
 | 
			
		||||
    console.log('页码无效:', page)
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  currentPage.value = page
 | 
			
		||||
  fetchMaps(page)
 | 
			
		||||
  // 滚动到页面顶部
 | 
			
		||||
  window.scrollTo({ top: 0, behavior: 'smooth' })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 跳转到地图详情
 | 
			
		||||
const goToMapDetail = (id) => {
 | 
			
		||||
  router.push(`/map/${id}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// 格式化日期
 | 
			
		||||
const formatDate = (dateString) => {
 | 
			
		||||
  return new Date(dateString).toLocaleDateString('zh-CN')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  console.log('组件挂载')
 | 
			
		||||
  fetchMaps(1)
 | 
			
		||||
  getAllTags().then(tags => {
 | 
			
		||||
    tagOptions.value = tags
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
 | 
			
		||||
</style> 
 | 
			
		||||
							
								
								
									
										1170
									
								
								src/views/WeaponMatch.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1170
									
								
								src/views/WeaponMatch.vue
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										96
									
								
								src/views/WeeklyRecommend.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/views/WeeklyRecommend.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
			
		||||
<template>
 | 
			
		||||
  <div class="weekly-recommend">
 | 
			
		||||
    <div class="page-header">
 | 
			
		||||
      <h1>每周地图推荐</h1>
 | 
			
		||||
      <div class="header-subtitle">
 | 
			
		||||
        <span class="date-range">{{ currentWeekRange }}</span>
 | 
			
		||||
      </div>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="table-container">
 | 
			
		||||
      <table class="maps-table">
 | 
			
		||||
        <thead>
 | 
			
		||||
          <tr>
 | 
			
		||||
            <th>序号</th>
 | 
			
		||||
            <th>预览图</th>
 | 
			
		||||
            <th>地图名称</th>
 | 
			
		||||
            <th>作者</th>
 | 
			
		||||
            <th>下载次数</th>
 | 
			
		||||
            <th>收藏次数</th>
 | 
			
		||||
            <th>玩家数量</th>
 | 
			
		||||
            <th>创建时间</th>
 | 
			
		||||
            <th>标签</th>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </thead>
 | 
			
		||||
        <tbody>
 | 
			
		||||
          <tr v-for="(map, index) in recommendedMaps" :key="map.id" @click="goToMapDetail(map.id)" class="table-row">
 | 
			
		||||
            <td class="rank-number">{{ index + 1 }}</td>
 | 
			
		||||
            <td class="preview-cell">
 | 
			
		||||
              <img :src="map.thumbnail" :alt="map.chinese_name">
 | 
			
		||||
            </td>
 | 
			
		||||
            <td class="map-name">{{ map.chinese_name }}</td>
 | 
			
		||||
            <td>{{ map.user }}</td>
 | 
			
		||||
            <td>{{ map.download_count }}</td>
 | 
			
		||||
            <td>{{ map.favourite_count }}</td>
 | 
			
		||||
            <td>{{ map.player_count }}</td>
 | 
			
		||||
            <td>{{ formatDate(map.create_time) }}</td>
 | 
			
		||||
            <td>
 | 
			
		||||
              <div class="tags">
 | 
			
		||||
                <span v-for="tag in map.tags" :key="tag" class="tag">{{ tag }}</span>
 | 
			
		||||
              </div>
 | 
			
		||||
            </td>
 | 
			
		||||
          </tr>
 | 
			
		||||
        </tbody>
 | 
			
		||||
      </table>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script setup>
 | 
			
		||||
import { ref, computed, onMounted } from 'vue'
 | 
			
		||||
import { useRouter } from 'vue-router'
 | 
			
		||||
import { getWeeklyTopMaps } from '../api/maps'
 | 
			
		||||
import '../assets/styles/common.css'
 | 
			
		||||
import '../assets/styles/WeeklyRecommend.css'
 | 
			
		||||
 | 
			
		||||
const router = useRouter()
 | 
			
		||||
const recommendedMaps = ref([])
 | 
			
		||||
 | 
			
		||||
// 获取当前周的范围
 | 
			
		||||
const currentWeekRange = computed(() => {
 | 
			
		||||
  const now = new Date()
 | 
			
		||||
  const start = new Date(now.setDate(now.getDate() - now.getDay()))
 | 
			
		||||
  const end = new Date(now.setDate(now.getDate() - now.getDay() + 6))
 | 
			
		||||
  return `${start.toLocaleDateString('zh-CN')} - ${end.toLocaleDateString('zh-CN')}`
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
const goToMapDetail = (id) => {
 | 
			
		||||
  router.push(`/map/${id}`)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const formatDate = (dateString) => {
 | 
			
		||||
  return new Date(dateString).toLocaleDateString('zh-CN')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const fetchRecommendedMaps = async () => {
 | 
			
		||||
  try {
 | 
			
		||||
    recommendedMaps.value = await getWeeklyTopMaps()
 | 
			
		||||
  } catch (error) {
 | 
			
		||||
    console.error('获取推荐地图失败:', error)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
  fetchRecommendedMaps()
 | 
			
		||||
})
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style scoped>
 | 
			
		||||
.rank-number {
 | 
			
		||||
  font-weight: bold;
 | 
			
		||||
  color: #1a237e;
 | 
			
		||||
  text-align: center;
 | 
			
		||||
  width: 50px;
 | 
			
		||||
  font-size: 1.1em;
 | 
			
		||||
}
 | 
			
		||||
</style> 
 | 
			
		||||
							
								
								
									
										16
									
								
								vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								vite.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
import { defineConfig } from 'vite'
 | 
			
		||||
import vue from '@vitejs/plugin-vue'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [vue()],
 | 
			
		||||
  resolve: {
 | 
			
		||||
    alias: {
 | 
			
		||||
      '@': path.resolve(__dirname, './src')
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  server: {
 | 
			
		||||
    port: 8082,
 | 
			
		||||
    open: true
 | 
			
		||||
  }
 | 
			
		||||
}) 
 | 
			
		||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user