DCFronted/src/views/index/EditorsMaps.vue
2025-06-24 22:54:04 +08:00

475 lines
9.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

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="maps">
<button class="back-button" @click="goBack">
<span class="back-icon"></span> 返回
</button>
<div class="page-header">
<h1>{{ authorName }} 的地图</h1>
</div>
<div class="filters-row">
<div class="filters">
<label>玩家数</label>
<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>
</div>
</div>
<div v-if="isLoading" class="loading-container">
<div class="loading-bar">
<div class="loading-progress"></div>
</div>
<div class="loading-text">加载中...</div>
</div>
<div v-else class="table-container">
<table class="maps-table">
<thead>
<tr>
<th>预览图</th>
<th>地图名称</th>
<th>下载次数</th>
<th>收藏次数</th>
<th>玩家数量</th>
<th>创建时间</th>
<th>标签</th>
</tr>
</thead>
<tbody>
<tr v-for="map in pagedMaps" :key="map.id" class="table-row" @click="goToMapDetail(map.id)">
<td class="preview-cell">
<img :src="map.thumbnail" :alt="map.chinese_name" class="preview-image" />
</td>
<td class="map-name">{{ map.chinese_name }}</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 v-if="!isLoading" 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="currentPage === totalPages"
@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>
<!-- 添加错误弹窗 -->
<ErrorDialog
:visible="showError"
:title="errorTitle"
:message="errorMessage"
@close="showError = false"
/>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { searchMapsByConditions } from '@/api/centre_maps.js'
import MapQuery from '@/utils/MapQuery.js'
import { getAllTags } from '@/api/maps.js'
import ErrorDialog from '@/components/ErrorDialog.vue'
import '../../assets/styles/common.css'
import '../../assets/styles/Maps.css'
const route = useRoute()
const router = useRouter()
const authorMaps = ref([])
const authorName = ref('')
const isLoading = ref(true)
// 搜索和筛选
const playerCountFilter = ref('')
// 错误弹窗相关
const showError = ref(false)
const errorTitle = ref('')
const errorMessage = ref('')
const currentPage = ref(1)
const pageSize = 20
const jumpPage = ref('')
const pagedMaps = computed(() => {
const start = (currentPage.value - 1) * pageSize
return authorMaps.value.slice(start, start + pageSize)
})
const totalPages = computed(() => {
return Math.ceil(authorMaps.value.length / pageSize) || 1
})
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 changePage = (page) => {
if (page < 1 || page > totalPages.value) return
currentPage.value = page
window.scrollTo({ top: 0, behavior: 'smooth' })
}
const handleJumpPage = () => {
const page = parseInt(jumpPage.value)
if (page && page >= 1 && page <= totalPages.value) {
changePage(page)
}
jumpPage.value = ''
}
const handleFilterChange = () => {
currentPage.value = 1
fetchAuthorMaps()
}
// 返回上一页
const goBack = () => {
router.push('/author')
}
// 格式化日期
const formatDate = (dateString) => {
if (!dateString) return '未知'
const date = new Date(dateString)
return date.toLocaleDateString()
}
const showErrorMessage = (message, title = '') => {
errorMessage.value = message
errorTitle.value = title
showError.value = true
}
// 获取作者的地图列表
const fetchAuthorMaps = async () => {
try {
isLoading.value = true
const author = route.query.author
if (!author) {
router.push('/author')
return
}
authorName.value = author
const query = new MapQuery().user(author)
if (playerCountFilter.value) {
query.equ('player_count', playerCountFilter.value)
}
const params = query.build()
const maps = await searchMapsByConditions(params)
authorMaps.value = maps
} catch (error) {
console.error('获取作者地图列表失败:', error)
showErrorMessage('获取作者地图列表失败,请稍后重试')
} finally {
isLoading.value = false
}
}
// 跳转到地图详情
const goToMapDetail = (mapId) => {
router.push(`/map/${mapId}`)
}
onMounted(() => {
fetchAuthorMaps()
})
</script>
<style scoped>
.maps-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.maps-table th,
.maps-table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #ddd;
}
.maps-table th {
background-color: #f5f5f5;
font-weight: bold;
}
.table-row:hover {
background-color: #f5f5f5;
cursor: pointer;
}
.preview-cell {
width: 120px;
}
.preview-image {
width: 80px;
height: 80px;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.map-name {
font-weight: bold;
color: #1a237e;
}
.tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
align-items: center;
}
.tag {
background: #e3f0ff;
color: #2563eb;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
margin-right: 2px;
}
.header-content {
display: flex;
align-items: center;
gap: 20px;
}
.back-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 4px;
color: #666;
font-size: 14px;
cursor: pointer;
transition: all 0.3s ease;
margin-bottom: 20px;
}
.back-button:hover {
background: #e0e0e0;
color: #333;
}
.back-icon {
font-size: 18px;
line-height: 1;
}
.pagination {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin: 30px 0 0 0;
}
.page-btn {
padding: 6px 16px;
border: none;
background: #e3f0ff;
color: #2563eb;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.page-btn:disabled {
background: #f5f5f5;
color: #aaa;
cursor: not-allowed;
}
.page-numbers {
display: flex;
gap: 4px;
}
.page-number {
padding: 6px 12px;
border: none;
background: #f5f5f5;
color: #2563eb;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.page-number.active {
background: #2563eb;
color: #fff;
}
.page-ellipsis {
padding: 6px 8px;
color: #aaa;
font-size: 14px;
}
.page-jump {
display: flex;
align-items: center;
gap: 4px;
}
.jump-input {
width: 50px;
padding: 4px 6px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.jump-btn {
padding: 4px 10px;
background: #2563eb;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
transition: background 0.2s;
}
.jump-btn:hover {
background: #1a237e;
}
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 0;
}
.loading-bar {
width: 300px;
height: 4px;
background-color: #f0f0f0;
border-radius: 2px;
overflow: hidden;
position: relative;
}
.loading-progress {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 30%;
background: linear-gradient(90deg, #2563eb, #1a237e);
border-radius: 2px;
animation: loading 1.5s ease-in-out infinite;
}
.loading-text {
margin-top: 12px;
color: #666;
font-size: 14px;
}
@keyframes loading {
0% {
left: -30%;
}
100% {
left: 100%;
}
}
@media screen and (max-width: 480px) {
.loading-bar {
width: 80%;
}
.loading-text {
font-size: 12px;
}
}
.filters-row {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 0;
}
.filters {
display: flex;
align-items: center;
gap: 12px;
}
.filters label {
font-size: 15px;
color: #222;
margin-right: 6px;
font-weight: 500;
}
.filter-select {
padding: 6px 12px;
border-radius: 4px;
border: 1px solid #ddd;
font-size: 15px;
background-color: #fff;
}
.filters-row, .filters {
box-shadow: none;
}
</style>