feat(首页): 新增资源收集关卡功能

- 在首页添加资源收集关卡卡片,展示资源收集相关数据
- 优化活动关卡卡片,增加结束时间和剩余时间显示
- 调整页面布局,使活动关卡和资源收集关卡卡片更加清晰
- 优化图片加载逻辑,增加错误处理
This commit is contained in:
2025-08-30 14:25:44 +08:00
parent 96ef72d300
commit ab7afb3d3b

View File

@@ -5,7 +5,7 @@
<div class="content">
<!-- 当期活动关卡 -->
<a-card title="当期活动关卡" class="activity-card" :loading="loading">
<a-card v-if="activityData?.length" title="当期活动关卡" class="activity-card" :loading="loading">
<template #extra>
<a-button type="text" @click="refreshActivity" :loading="loading">
<template #icon>
@@ -31,57 +31,80 @@
<ClockCircleOutlined class="time-icon" />
<span class="time-label">结束时间</span>
<span class="time-value">{{
formatTime(currentActivity.UtcExpireTime, currentActivity.TimeZone)
formatTime(currentActivity.UtcExpireTime)
}}</span>
</div>
</div>
<div class="activity-right">
<a-statistic-countdown
title="当期活动剩余时间"
:value="getCountdownValue(currentActivity.UtcExpireTime)"
format="D 天 H 时 m 分"
:value-style="getCountdownStyle(currentActivity.UtcExpireTime)"
@finish="onCountdownFinish"
/>
<a-statistic-countdown title="当期活动剩余时间" :value="getCountdownValue(currentActivity.UtcExpireTime)"
format="D 天 H 时 m 分" :value-style="getCountdownStyle(currentActivity.UtcExpireTime)"
@finish="onCountdownFinish" />
</div>
</div>
</div>
<div v-if="activityData?.length" class="activity-list">
<div class="activity-list">
<div v-for="item in activityData" :key="item.Value" class="activity-item">
<div class="stage-info">
<div class="stage-name">{{ item.Display }}</div>
<!-- <div class="stage-value">{{ item.Value }}</div>-->
</div>
<div class="drop-info">
<div class="drop-image">
<img
:src="
item.DropName.startsWith('DESC:')
? getMaterialImage('固源岩')
: getMaterialImage(item.DropName)
"
:alt="item.DropName.startsWith('DESC:') ? '固源岩' : item.DropName"
@error="handleImageError"
/>
<img v-if="getMaterialImage(item.DropName.startsWith('DESC:') ? '固源岩' : item.DropName)" :src="item.DropName.startsWith('DESC:')
? getMaterialImage('固源岩')
: getMaterialImage(item.DropName)
" :alt="item.DropName.startsWith('DESC:') ? '固源岩' : item.DropName" @error="handleImageError" />
</div>
<div class="drop-details">
<div class="drop-name">
{{ item.DropName.startsWith('DESC:') ? item.DropName.substring(5) : item.DropName }}
</div>
<!-- <div v-if="item.Drop && !item.DropName.startsWith('DESC:')" class="drop-id">-->
<!-- ID: {{ item.Drop }}-->
<!-- </div>-->
</div>
</div>
</div>
</div>
</a-card>
<!-- 资源收集关卡 -->
<a-card title="今日开放资源收集关卡" class="resource-card" :loading="loading">
<template #extra>
<a-button type="text" @click="refreshActivity" :loading="loading">
<template #icon>
<ReloadOutlined />
</template>
刷新
</a-button>
</template>
<div v-if="error" class="error-message">
<a-alert :message="error" type="error" show-icon closable @close="error = ''" />
</div>
<div v-if="resourceData?.length" class="resource-list">
<div v-for="item in resourceData" :key="item.Value" class="resource-item">
<div class="stage-info">
<div class="stage-name">{{ item.Display }}</div>
</div>
<div class="drop-info">
<div class="drop-image">
<img v-if="getMaterialImage(item.DropName)" :src="getMaterialImage(item.DropName)" :alt="item.DropName"
@error="handleImageError" />
</div>
<div class="drop-details">
<div class="drop-name">{{ item.DropName }}</div>
<div class="drop-tip">{{ item.Activity.Tip }}</div>
</div>
</div>
</div>
</div>
<div v-else-if="!loading" class="empty-state">
<a-empty description="暂无活动关卡数据" />
<a-empty description="暂无资源收集数据" />
</div>
</a-card>
@@ -110,10 +133,7 @@
<div class="proxy-stats">
<!-- 第一行最后代理时间独占一行 -->
<div class="stat-item full-width">
<a-statistic
title="最后代理时间"
:value="formatProxyDisplay(proxy.LastProxyDate)"
/>
<a-statistic title="最后代理时间" :value="formatProxyDisplay(proxy.LastProxyDate)" />
</div>
<!-- 第二行代理次数 错误次数 -->
@@ -121,11 +141,8 @@
<a-statistic title="代理次数" :value="proxy.ProxyTimes" />
</div>
<div class="stat-item half-width">
<a-statistic
title="错误次数"
:value="proxy.ErrorTimes"
:value-style="{ color: proxy.ErrorTimes > 0 ? '#ff4d4f' : undefined }"
/>
<a-statistic title="错误次数" :value="proxy.ErrorTimes"
:value-style="{ color: proxy.ErrorTimes > 0 ? '#ff4d4f' : undefined }" />
</div>
</div>
@@ -165,8 +182,6 @@ import { ref, onMounted, computed } from 'vue'
import { message } from 'ant-design-vue'
import {
ReloadOutlined,
InfoCircleOutlined,
CalendarOutlined,
ClockCircleOutlined,
UserOutlined,
} from '@ant-design/icons-vue'
@@ -197,15 +212,30 @@ interface ProxyInfo {
}
interface ApiResponse {
Stage: ActivityItem[]
Stage: {
Activity: ActivityItem[]
Resource: ResourceItem[]
}
Proxy: Record<string, ProxyInfo>
}
interface ResourceItem {
Display: string
Value: string
DropName: string
Activity: {
Tip: string
StageName: string
}
}
const loading = ref(false)
const error = ref('')
const activityData = ref<ActivityItem[]>([])
const resourceData = ref<ResourceItem[]>([])
const proxyData = ref<Record<string, ProxyInfo>>({})
// 获取当前活动信息
const currentActivity = computed(() => {
if (!activityData.value.length) return null
@@ -218,7 +248,7 @@ const formatProxyDisplay = (dateStr: string) => {
}
// 格式化时间显示 - 直接使用给定时间,不进行时区转换
const formatTime = (timeString: string, timeZone: number) => {
const formatTime = (timeString: string) => {
try {
// 直接使用给定的时间字符串,因为已经是中国时间
const date = new Date(timeString)
@@ -307,9 +337,13 @@ const onCountdownFinish = () => {
}
const getMaterialImage = (dropName: string) => {
if (!dropName) {
return ''
}
try {
return new URL(`../assets/materials/${dropName}.png`, import.meta.url).href
} catch {
} catch (error) {
console.warn('Failed to load material image:', dropName, error)
return ''
}
}
@@ -329,7 +363,8 @@ const fetchActivityData = async () => {
if (response.code === 200) {
const data = response.data as ApiResponse
if (data.Stage) {
activityData.value = data.Stage
activityData.value = data.Stage.Activity || []
resourceData.value = data.Stage.Resource || []
}
if (data.Proxy) {
proxyData.value = data.Proxy
@@ -394,11 +429,49 @@ onMounted(() => {
margin-bottom: 24px;
}
.resource-card {
margin-bottom: 24px;
}
.activity-card :deep(.ant-card-head-title) {
font-size: 18px;
font-weight: 600;
}
.resource-card :deep(.ant-card-head-title) {
font-size: 18px;
font-weight: 600;
}
.resource-list {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
}
.resource-item {
display: flex;
align-items: center;
padding: 16px;
background: var(--ant-color-bg-container);
border: 1px solid var(--ant-color-border);
border-radius: 8px;
transition: all 0.2s ease;
}
.resource-item:hover {
border-color: var(--ant-color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.drop-tip {
font-size: 12px;
color: var(--ant-color-text-tertiary);
margin-top: 2px;
}
.error-message {
margin-bottom: 16px;
}
@@ -568,13 +641,17 @@ onMounted(() => {
}
@media (max-width: 1200px) {
.activity-list {
.activity-list,
.resource-list {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 900px) {
.activity-list {
.activity-list,
.resource-list {
grid-template-columns: repeat(2, 1fr);
}
}
@@ -649,11 +726,13 @@ onMounted(() => {
}
.stat-item.full-width {
flex: 0 0 100%; /* 占满整行 */
flex: 0 0 100%;
/* 占满整行 */
}
.stat-item.half-width {
flex: 0 0 calc(50% - 8px); /* 每个占一半宽度,减去间距 */
flex: 0 0 calc(50% - 8px);
/* 每个占一半宽度,减去间距 */
}
.stat-item {
@@ -677,11 +756,13 @@ onMounted(() => {
padding: 16px;
}
.activity-list {
.activity-list,
.resource-list {
grid-template-columns: 1fr;
}
.activity-item {
.activity-item,
.resource-item {
padding: 12px;
}