feat: 更新激励中心页面,新增徽章、称号、奖励记录页面
This commit is contained in:
parent
c1ac3a9fed
commit
d82ae4e072
@ -144,6 +144,30 @@ const routes: RouteRecordRaw[] = [
|
||||
title: '激励中心'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/my-badges',
|
||||
name: 'MyBadges',
|
||||
component: () => import('@/views/MyBadges.vue'),
|
||||
meta: {
|
||||
title: '我的徽章'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/my-titles',
|
||||
name: 'MyTitles',
|
||||
component: () => import('@/views/MyTitles.vue'),
|
||||
meta: {
|
||||
title: '我的称号'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/reward-records',
|
||||
name: 'RewardRecords',
|
||||
component: () => import('@/views/RewardRecords.vue'),
|
||||
meta: {
|
||||
title: '奖励记录'
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/admin/tasks',
|
||||
name: 'AdminTaskManagement',
|
||||
|
||||
@ -23,6 +23,10 @@
|
||||
<span class="stat-value">{{ continuousCheckIn }}</span>
|
||||
<span class="stat-label">连续签到</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">{{ badgeCount }}</span>
|
||||
<span class="stat-label">获得徽章</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">{{ achievementCount }}</span>
|
||||
<span class="stat-label">已获成就</span>
|
||||
@ -35,6 +39,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前称号展示 -->
|
||||
<div class="active-title-card" v-if="activeTitle">
|
||||
<div class="title-badge" :style="{ backgroundColor: activeTitle.titleColor + '20', borderColor: activeTitle.titleColor }">
|
||||
<span class="title-name" :style="{ color: activeTitle.titleColor }">{{ activeTitle.titleName }}</span>
|
||||
</div>
|
||||
<span class="title-desc">当前佩戴的称号</span>
|
||||
<a-button type="link" size="small" @click="router.push('/my-titles')">更换称号</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 功能模块入口 -->
|
||||
<div class="module-grid">
|
||||
<!-- 每日签到 -->
|
||||
@ -55,10 +68,40 @@
|
||||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||||
</div>
|
||||
|
||||
<!-- 我的徽章 -->
|
||||
<div class="module-card badge" @click="router.push('/my-badges')">
|
||||
<div class="module-icon">
|
||||
<Icon name="star" size="xl" color="#FFD700" />
|
||||
</div>
|
||||
<div class="module-info">
|
||||
<h3 class="module-title">我的徽章</h3>
|
||||
<p class="module-desc">收集徽章,展示成就</p>
|
||||
</div>
|
||||
<div class="module-progress" v-if="badgeCount > 0">
|
||||
<span>已获 {{ badgeCount }} 个</span>
|
||||
</div>
|
||||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||||
</div>
|
||||
|
||||
<!-- 我的称号 -->
|
||||
<div class="module-card title" @click="router.push('/my-titles')">
|
||||
<div class="module-icon">
|
||||
<Icon name="badge" size="xl" color="#8B5CF6" />
|
||||
</div>
|
||||
<div class="module-info">
|
||||
<h3 class="module-title">我的称号</h3>
|
||||
<p class="module-desc">解锁称号,彰显身份</p>
|
||||
</div>
|
||||
<div class="module-progress" v-if="titleCount > 0">
|
||||
<span>已有 {{ titleCount }} 个</span>
|
||||
</div>
|
||||
<Icon name="chevronRight" size="md" color="#6B6B6B" class="module-arrow" />
|
||||
</div>
|
||||
|
||||
<!-- 成就中心 -->
|
||||
<div class="module-card achievement" @click="router.push('/achievements')">
|
||||
<div class="module-icon">
|
||||
<Icon name="trophy" size="xl" color="#FFD700" />
|
||||
<Icon name="trophy" size="xl" color="#31f08b" />
|
||||
</div>
|
||||
<div class="module-info">
|
||||
<h3 class="module-title">成就中心</h3>
|
||||
@ -118,7 +161,7 @@
|
||||
<!-- 排行榜 -->
|
||||
<div class="module-card leaderboard" @click="router.push('/designer-hall')">
|
||||
<div class="module-icon">
|
||||
<Icon name="chartBar" size="xl" color="#8B5CF6" />
|
||||
<Icon name="chartBar" size="xl" color="#10B981" />
|
||||
</div>
|
||||
<div class="module-info">
|
||||
<h3 class="module-title">排行榜</h3>
|
||||
@ -131,30 +174,52 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 积分记录 -->
|
||||
<!-- 奖励记录 -->
|
||||
<div class="section">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<Icon name="clock" size="lg" color="#AFAFAF" style="margin-right: 8px;" />
|
||||
积分记录
|
||||
奖励记录
|
||||
</h2>
|
||||
<a-button type="link" @click="viewAllRecords">查看全部</a-button>
|
||||
<a-button type="link" @click="router.push('/reward-records')">查看全部</a-button>
|
||||
</div>
|
||||
<div class="record-list">
|
||||
<div class="record-list" v-if="!recordsLoading">
|
||||
<div v-for="record in recentRecords" :key="record.id" class="record-item">
|
||||
<div class="record-info">
|
||||
<Icon :name="getRecordIcon(record.type)" size="md" :color="record.score > 0 ? '#31f08b' : '#EF4444'" />
|
||||
<Icon :name="getRecordIcon(record.rewardType)" size="md" color="#31f08b" />
|
||||
<div class="record-detail">
|
||||
<span class="record-title">{{ record.title }}</span>
|
||||
<span class="record-time">{{ formatTime(record.time) }}</span>
|
||||
<span class="record-title">{{ record.ruleName || '系统奖励' }}</span>
|
||||
<span class="record-time">{{ formatTime(record.createTime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :class="['record-score', record.score > 0 ? 'positive' : 'negative']">
|
||||
{{ record.score > 0 ? '+' : '' }}{{ record.score }}
|
||||
<div class="record-score positive">
|
||||
{{ parseRewardValue(record.rewardValue) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="recentRecords.length === 0" class="empty-records">
|
||||
暂无积分记录
|
||||
暂无奖励记录,快去完成任务获取奖励吧!
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="loading-records">
|
||||
<a-spin />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 我的徽章展示 -->
|
||||
<div class="section" v-if="myBadges.length > 0">
|
||||
<div class="section-header">
|
||||
<h2 class="section-title">
|
||||
<Icon name="star" size="lg" color="#FFD700" style="margin-right: 8px;" />
|
||||
我的徽章
|
||||
</h2>
|
||||
<a-button type="link" @click="router.push('/my-badges')">查看全部</a-button>
|
||||
</div>
|
||||
<div class="badges-showcase">
|
||||
<div v-for="badge in myBadges.slice(0, 6)" :key="badge.id" class="badge-item">
|
||||
<div class="badge-icon" :style="{ backgroundColor: badge.badgeColor }">
|
||||
{{ badge.badgeName.charAt(0) }}
|
||||
</div>
|
||||
<span class="badge-name">{{ badge.badgeName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -174,6 +239,15 @@ import { hasCheckedInToday, getCheckInStats } from '@/api/incentive'
|
||||
import { getMyGoals } from '@/api/goal'
|
||||
import { getContestList } from '@/api/contest'
|
||||
import { getTeamPKList } from '@/api/teamPK'
|
||||
import {
|
||||
getMyRewardRecords,
|
||||
getMyBadges,
|
||||
getMyTitles,
|
||||
getMyActiveTitle,
|
||||
type RewardRecord,
|
||||
type UserBadge,
|
||||
type UserTitle
|
||||
} from '@/api/rewardRule'
|
||||
|
||||
const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
@ -189,18 +263,15 @@ const myRank = ref(0)
|
||||
const unclaimedAchievements = ref(0)
|
||||
const hasCheckedIn = ref(false)
|
||||
|
||||
// 积分记录
|
||||
const recentRecords = ref<Array<{
|
||||
id: number
|
||||
type: string
|
||||
title: string
|
||||
score: number
|
||||
time: string
|
||||
}>>([
|
||||
{ id: 1, type: 'checkin', title: '每日签到奖励', score: 10, time: new Date().toISOString() },
|
||||
{ id: 2, type: 'task', title: '完成任务获得积分', score: 50, time: new Date(Date.now() - 86400000).toISOString() },
|
||||
{ id: 3, type: 'achievement', title: '解锁成就「初出茅庐」', score: 100, time: new Date(Date.now() - 172800000).toISOString() },
|
||||
])
|
||||
// 新增:徽章和称号数据
|
||||
const badgeCount = ref(0)
|
||||
const titleCount = ref(0)
|
||||
const myBadges = ref<UserBadge[]>([])
|
||||
const activeTitle = ref<UserTitle | null>(null)
|
||||
|
||||
// 奖励记录
|
||||
const recordsLoading = ref(false)
|
||||
const recentRecords = ref<RewardRecord[]>([])
|
||||
|
||||
const loadData = async () => {
|
||||
try {
|
||||
@ -224,18 +295,41 @@ const loadData = async () => {
|
||||
activePKCount.value = pks.records?.length || 0
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载数据失败:', error)
|
||||
console.error('加载基础数据失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const loadRewardData = async () => {
|
||||
try {
|
||||
// 加载奖励记录
|
||||
recordsLoading.value = true
|
||||
const recordsResult = await getMyRewardRecords({ page: 1, pageSize: 5 })
|
||||
recentRecords.value = recordsResult.records || []
|
||||
|
||||
// 加载我的徽章
|
||||
myBadges.value = await getMyBadges()
|
||||
badgeCount.value = myBadges.value.length
|
||||
|
||||
// 加载我的称号
|
||||
const titles = await getMyTitles()
|
||||
titleCount.value = titles.length
|
||||
|
||||
// 加载当前激活称号
|
||||
activeTitle.value = await getMyActiveTitle()
|
||||
|
||||
} catch (error) {
|
||||
console.error('加载奖励数据失败:', error)
|
||||
} finally {
|
||||
recordsLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const getRecordIcon = (type: string) => {
|
||||
const iconMap: Record<string, string> = {
|
||||
checkin: 'calendar',
|
||||
task: 'document',
|
||||
achievement: 'trophy',
|
||||
contest: 'fire',
|
||||
pk: 'bolt',
|
||||
goal: 'flag'
|
||||
score: 'star',
|
||||
badge: 'star',
|
||||
title: 'badge',
|
||||
exp: 'zap'
|
||||
}
|
||||
return iconMap[type] || 'star'
|
||||
}
|
||||
@ -244,19 +338,31 @@ const formatTime = (time: string) => {
|
||||
return dayjs(time).format('MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const viewAllRecords = () => {
|
||||
// TODO: 跳转到积分记录详情页
|
||||
const parseRewardValue = (value: string) => {
|
||||
try {
|
||||
const obj = JSON.parse(value)
|
||||
if (obj.score) return `+${obj.score} 积分`
|
||||
if (obj.badge) return `获得徽章`
|
||||
if (obj.title) return `获得称号`
|
||||
return '+奖励'
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
loadRewardData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.incentive-center {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: $bg-primary;
|
||||
}
|
||||
|
||||
.container {
|
||||
@ -271,23 +377,23 @@ onMounted(() => {
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: #AFAFAF;
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.score-card {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border: 1px solid #3A3A3A;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 16px;
|
||||
padding: 32px;
|
||||
margin-bottom: 32px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@ -295,7 +401,7 @@ onMounted(() => {
|
||||
|
||||
.score-main {
|
||||
.score-label {
|
||||
color: #AFAFAF;
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
@ -310,7 +416,7 @@ onMounted(() => {
|
||||
|
||||
.score-stats {
|
||||
display: flex;
|
||||
gap: 40px;
|
||||
gap: 32px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
@ -320,17 +426,46 @@ onMounted(() => {
|
||||
display: block;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
color: #6B6B6B;
|
||||
color: $text-tertiary;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
// 当前称号卡片
|
||||
.active-title-card {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 16px 20px;
|
||||
margin-bottom: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
|
||||
.title-badge {
|
||||
padding: 6px 16px;
|
||||
border-radius: 20px;
|
||||
border: 2px solid;
|
||||
}
|
||||
|
||||
.title-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.title-desc {
|
||||
color: $text-tertiary;
|
||||
font-size: 13px;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.module-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
@ -339,8 +474,8 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.module-card {
|
||||
background: #2A2A2A;
|
||||
border: 1px solid #3A3A3A;
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
@ -351,16 +486,18 @@ onMounted(() => {
|
||||
position: relative;
|
||||
|
||||
&:hover {
|
||||
border-color: #31f08b;
|
||||
border-color: $brand-primary;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
&.checkin:hover { border-color: #31f08b; }
|
||||
&.achievement:hover { border-color: #FFD700; }
|
||||
&.badge:hover { border-color: #FFD700; }
|
||||
&.title:hover { border-color: #8B5CF6; }
|
||||
&.achievement:hover { border-color: #31f08b; }
|
||||
&.goals:hover { border-color: #3B82F6; }
|
||||
&.contest:hover { border-color: #EF4444; }
|
||||
&.team-pk:hover { border-color: #F59E0B; }
|
||||
&.leaderboard:hover { border-color: #8B5CF6; }
|
||||
&.leaderboard:hover { border-color: #10B981; }
|
||||
}
|
||||
|
||||
.module-icon {
|
||||
@ -380,19 +517,19 @@ onMounted(() => {
|
||||
.module-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.module-desc {
|
||||
font-size: 13px;
|
||||
color: #6B6B6B;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.module-status, .module-progress, .module-rank {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
color: #AFAFAF;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.module-badge {
|
||||
@ -428,14 +565,14 @@ onMounted(() => {
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.record-list {
|
||||
background: #2A2A2A;
|
||||
border: 1px solid #3A3A3A;
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@ -445,7 +582,7 @@ onMounted(() => {
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #3A3A3A;
|
||||
border-bottom: 1px solid $border-primary;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@ -464,12 +601,12 @@ onMounted(() => {
|
||||
}
|
||||
|
||||
.record-title {
|
||||
color: #ECECEC;
|
||||
color: $text-primary;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
color: #6B6B6B;
|
||||
color: $text-tertiary;
|
||||
font-size: 12px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
@ -479,7 +616,7 @@ onMounted(() => {
|
||||
font-weight: 600;
|
||||
|
||||
&.positive {
|
||||
color: #31f08b;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
@ -487,10 +624,45 @@ onMounted(() => {
|
||||
}
|
||||
}
|
||||
|
||||
.empty-records {
|
||||
.empty-records, .loading-records {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: #6B6B6B;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
// 徽章展示区
|
||||
.badges-showcase {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.badge-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
.badge-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.badge-name {
|
||||
font-size: 12px;
|
||||
color: $text-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@ -503,5 +675,16 @@ onMounted(() => {
|
||||
gap: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.score-stats {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.active-title-card {
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
270
frontend/src/views/MyBadges.vue
Normal file
270
frontend/src/views/MyBadges.vue
Normal file
@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<div class="my-badges-page">
|
||||
<div class="container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<a-button type="text" @click="router.back()" class="back-btn">
|
||||
<Icon name="chevronLeft" size="md" color="#AFAFAF" />
|
||||
</a-button>
|
||||
<div class="header-info">
|
||||
<h1 class="page-title">
|
||||
<Icon name="star" size="xl" color="#FFD700" style="margin-right: 12px;" />
|
||||
我的徽章
|
||||
</h1>
|
||||
<p class="page-desc">收集徽章,展示你的成就</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计信息 -->
|
||||
<div class="stats-bar">
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ myBadges.length }}</span>
|
||||
<span class="stat-label">已获得</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ allBadges.length }}</span>
|
||||
<span class="stat-label">全部徽章</span>
|
||||
</div>
|
||||
<div class="stat-item">
|
||||
<span class="stat-value">{{ progressPercent }}%</span>
|
||||
<span class="stat-label">收集进度</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 徽章展示 -->
|
||||
<div class="badges-section">
|
||||
<h2 class="section-title">全部徽章</h2>
|
||||
<div class="badges-grid" v-if="!loading">
|
||||
<div
|
||||
v-for="badge in allBadges"
|
||||
:key="badge.badgeCode"
|
||||
:class="['badge-card', { 'unlocked': isUnlocked(badge.badgeCode) }]"
|
||||
>
|
||||
<div class="badge-icon" :style="{ backgroundColor: isUnlocked(badge.badgeCode) ? badge.badgeColor : '#3A3A3A' }">
|
||||
{{ badge.badgeName.charAt(0) }}
|
||||
</div>
|
||||
<div class="badge-info">
|
||||
<h3 class="badge-name">{{ badge.badgeName }}</h3>
|
||||
<p class="badge-desc">{{ badge.description }}</p>
|
||||
<a-tag v-if="isUnlocked(badge.badgeCode)" color="green">已获得</a-tag>
|
||||
<a-tag v-else color="default">未解锁</a-tag>
|
||||
</div>
|
||||
<div class="badge-time" v-if="getUnlockTime(badge.badgeCode)">
|
||||
{{ formatTime(getUnlockTime(badge.badgeCode)!) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="loading-container">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
getMyBadges,
|
||||
getAllBadges,
|
||||
type UserBadge,
|
||||
type BadgeDefinition
|
||||
} from '@/api/rewardRule'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const myBadges = ref<UserBadge[]>([])
|
||||
const allBadges = ref<BadgeDefinition[]>([])
|
||||
|
||||
const progressPercent = computed(() => {
|
||||
if (allBadges.value.length === 0) return 0
|
||||
return Math.round((myBadges.value.length / allBadges.value.length) * 100)
|
||||
})
|
||||
|
||||
const isUnlocked = (badgeCode: string) => {
|
||||
return myBadges.value.some(b => b.badgeCode === badgeCode)
|
||||
}
|
||||
|
||||
const getUnlockTime = (badgeCode: string): string | undefined => {
|
||||
const badge = myBadges.value.find(b => b.badgeCode === badgeCode)
|
||||
return badge?.createTime
|
||||
}
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
return dayjs(time).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [badges, all] = await Promise.all([
|
||||
getMyBadges(),
|
||||
getAllBadges()
|
||||
])
|
||||
myBadges.value = badges
|
||||
allBadges.value = all
|
||||
} catch (error) {
|
||||
console.error('加载徽章数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.my-badges-page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: $bg-primary;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-bar {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-item {
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
display: block;
|
||||
font-size: 32px;
|
||||
font-weight: 700;
|
||||
color: #FFD700;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
color: $text-tertiary;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.badges-section {
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.badges-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.badge-card {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
opacity: 0.5;
|
||||
transition: all 0.3s;
|
||||
|
||||
&.unlocked {
|
||||
opacity: 1;
|
||||
border-color: #FFD700;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(255, 215, 0, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
.badge-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.badge-info {
|
||||
flex: 1;
|
||||
|
||||
.badge-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.badge-desc {
|
||||
font-size: 13px;
|
||||
color: $text-secondary;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.badge-time {
|
||||
font-size: 12px;
|
||||
color: $text-tertiary;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
}
|
||||
</style>
|
||||
|
||||
318
frontend/src/views/MyTitles.vue
Normal file
318
frontend/src/views/MyTitles.vue
Normal file
@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<div class="my-titles-page">
|
||||
<div class="container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<a-button type="text" @click="router.back()" class="back-btn">
|
||||
<Icon name="chevronLeft" size="md" color="#AFAFAF" />
|
||||
</a-button>
|
||||
<div class="header-info">
|
||||
<h1 class="page-title">
|
||||
<Icon name="badge" size="xl" color="#8B5CF6" style="margin-right: 12px;" />
|
||||
我的称号
|
||||
</h1>
|
||||
<p class="page-desc">选择一个称号展示在你的名字旁边</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 当前称号 -->
|
||||
<div class="current-title-card">
|
||||
<div class="card-label">当前佩戴</div>
|
||||
<div class="current-title" v-if="activeTitle">
|
||||
<span class="title-name" :style="{ color: activeTitle.titleColor }">
|
||||
{{ activeTitle.titleName }}
|
||||
</span>
|
||||
<a-button type="link" size="small" @click="handleRemoveTitle">取消佩戴</a-button>
|
||||
</div>
|
||||
<div class="no-title" v-else>
|
||||
<span>暂未佩戴称号</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 称号列表 -->
|
||||
<div class="titles-section">
|
||||
<h2 class="section-title">我的称号</h2>
|
||||
<div class="titles-grid" v-if="!loading">
|
||||
<div
|
||||
v-for="title in myTitles"
|
||||
:key="title.id"
|
||||
:class="['title-card', { 'active': activeTitle?.id === title.id }]"
|
||||
@click="handleSelectTitle(title)"
|
||||
>
|
||||
<div class="title-badge" :style="{ backgroundColor: title.titleColor + '20', borderColor: title.titleColor }">
|
||||
<span class="title-name" :style="{ color: title.titleColor }">{{ title.titleName }}</span>
|
||||
</div>
|
||||
<p class="title-desc">{{ title.description }}</p>
|
||||
<div class="title-meta">
|
||||
<span class="title-time">获得于 {{ formatTime(title.createTime) }}</span>
|
||||
<a-tag v-if="activeTitle?.id === title.id" color="purple">使用中</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="myTitles.length === 0" class="empty-titles">
|
||||
暂无称号,完成任务获取称号吧!
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="loading-container">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 全部称号 -->
|
||||
<div class="titles-section">
|
||||
<h2 class="section-title">全部称号</h2>
|
||||
<div class="titles-grid">
|
||||
<div
|
||||
v-for="title in allTitles"
|
||||
:key="title.titleCode"
|
||||
:class="['title-card', 'all', { 'unlocked': isUnlocked(title.titleCode) }]"
|
||||
>
|
||||
<div class="title-badge" :style="{
|
||||
backgroundColor: isUnlocked(title.titleCode) ? title.titleColor + '20' : '#2A2A2A',
|
||||
borderColor: isUnlocked(title.titleCode) ? title.titleColor : '#3A3A3A'
|
||||
}">
|
||||
<span class="title-name" :style="{ color: isUnlocked(title.titleCode) ? title.titleColor : '#6B6B6B' }">
|
||||
{{ title.titleName }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="title-desc">{{ title.description }}</p>
|
||||
<a-tag v-if="isUnlocked(title.titleCode)" color="green">已解锁</a-tag>
|
||||
<a-tag v-else color="default">未解锁</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import { message } from 'ant-design-vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
getMyTitles,
|
||||
getAllTitles,
|
||||
getMyActiveTitle,
|
||||
setActiveTitle,
|
||||
type UserTitle,
|
||||
type TitleDefinition
|
||||
} from '@/api/rewardRule'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const myTitles = ref<UserTitle[]>([])
|
||||
const allTitles = ref<TitleDefinition[]>([])
|
||||
const activeTitle = ref<UserTitle | null>(null)
|
||||
|
||||
const isUnlocked = (titleCode: string) => {
|
||||
return myTitles.value.some(t => t.titleCode === titleCode)
|
||||
}
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
return dayjs(time).format('YYYY-MM-DD')
|
||||
}
|
||||
|
||||
const handleSelectTitle = async (title: UserTitle) => {
|
||||
try {
|
||||
await setActiveTitle(title.id)
|
||||
activeTitle.value = title
|
||||
message.success('称号已更换')
|
||||
} catch (error) {
|
||||
message.error('更换失败')
|
||||
}
|
||||
}
|
||||
|
||||
const handleRemoveTitle = async () => {
|
||||
try {
|
||||
await setActiveTitle(undefined)
|
||||
activeTitle.value = null
|
||||
message.success('已取消佩戴')
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const [titles, all, active] = await Promise.all([
|
||||
getMyTitles(),
|
||||
getAllTitles(),
|
||||
getMyActiveTitle()
|
||||
])
|
||||
myTitles.value = titles
|
||||
allTitles.value = all
|
||||
activeTitle.value = active
|
||||
} catch (error) {
|
||||
console.error('加载称号数据失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.my-titles-page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: $bg-primary;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.current-title-card {
|
||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin-bottom: 32px;
|
||||
|
||||
.card-label {
|
||||
font-size: 12px;
|
||||
color: $text-tertiary;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.current-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
.title-name {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.no-title {
|
||||
color: $text-tertiary;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.titles-section {
|
||||
margin-bottom: 32px;
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.titles-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.title-card {
|
||||
background: $bg-card;
|
||||
border: 2px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
|
||||
&:hover {
|
||||
border-color: #8B5CF6;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: #8B5CF6;
|
||||
background: rgba(139, 92, 246, 0.1);
|
||||
}
|
||||
|
||||
&.all {
|
||||
cursor: default;
|
||||
|
||||
&:not(.unlocked) {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.title-badge {
|
||||
display: inline-block;
|
||||
padding: 8px 20px;
|
||||
border-radius: 20px;
|
||||
border: 2px solid;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title-name {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.title-desc {
|
||||
font-size: 13px;
|
||||
color: $text-secondary;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.title-meta {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.title-time {
|
||||
font-size: 12px;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-titles {
|
||||
grid-column: 1 / -1;
|
||||
text-align: center;
|
||||
padding: 48px;
|
||||
color: $text-tertiary;
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px;
|
||||
}
|
||||
</style>
|
||||
|
||||
264
frontend/src/views/RewardRecords.vue
Normal file
264
frontend/src/views/RewardRecords.vue
Normal file
@ -0,0 +1,264 @@
|
||||
<template>
|
||||
<DefaultLayout>
|
||||
<div class="reward-records-page">
|
||||
<div class="container">
|
||||
<!-- 页面标题 -->
|
||||
<div class="page-header">
|
||||
<a-button type="text" @click="router.back()" class="back-btn">
|
||||
<Icon name="chevronLeft" size="md" color="#AFAFAF" />
|
||||
</a-button>
|
||||
<div class="header-info">
|
||||
<h1 class="page-title">
|
||||
<Icon name="clock" size="xl" color="#31f08b" style="margin-right: 12px;" />
|
||||
奖励记录
|
||||
</h1>
|
||||
<p class="page-desc">查看你获得的所有奖励</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计卡片 -->
|
||||
<div class="stats-row">
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ totalScore }}</div>
|
||||
<div class="stat-label">累计获得积分</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-value">{{ totalCount }}</div>
|
||||
<div class="stat-label">获奖次数</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 记录列表 -->
|
||||
<div class="records-section">
|
||||
<a-table
|
||||
:columns="columns"
|
||||
:data-source="records"
|
||||
:loading="loading"
|
||||
:pagination="{
|
||||
current: currentPage,
|
||||
pageSize: pageSize,
|
||||
total: total,
|
||||
onChange: handlePageChange,
|
||||
showSizeChanger: false
|
||||
}"
|
||||
row-key="id"
|
||||
>
|
||||
<template #bodyCell="{ column, record }">
|
||||
<template v-if="column.key === 'rewardType'">
|
||||
<a-tag :color="getTypeColor(record.rewardType)">
|
||||
{{ getTypeName(record.rewardType) }}
|
||||
</a-tag>
|
||||
</template>
|
||||
<template v-if="column.key === 'rewardValue'">
|
||||
<span class="reward-value">{{ parseRewardValue(record.rewardValue) }}</span>
|
||||
</template>
|
||||
<template v-if="column.key === 'createTime'">
|
||||
{{ formatTime(record.createTime) }}
|
||||
</template>
|
||||
</template>
|
||||
</a-table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayout>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import DefaultLayout from '@/layouts/DefaultLayout.vue'
|
||||
import Icon from '@/components/Icon.vue'
|
||||
import dayjs from 'dayjs'
|
||||
import {
|
||||
getMyRewardRecords,
|
||||
type RewardRecord
|
||||
} from '@/api/rewardRule'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loading = ref(false)
|
||||
const records = ref<RewardRecord[]>([])
|
||||
const currentPage = ref(1)
|
||||
const pageSize = ref(20)
|
||||
const total = ref(0)
|
||||
|
||||
const columns = [
|
||||
{ title: '奖励规则', dataIndex: 'ruleName', key: 'ruleName' },
|
||||
{ title: '奖励类型', dataIndex: 'rewardType', key: 'rewardType', width: 120 },
|
||||
{ title: '奖励内容', dataIndex: 'rewardValue', key: 'rewardValue', width: 150 },
|
||||
{ title: '获得时间', dataIndex: 'createTime', key: 'createTime', width: 180 },
|
||||
]
|
||||
|
||||
const totalScore = computed(() => {
|
||||
return records.value.reduce((sum, r) => {
|
||||
try {
|
||||
const obj = JSON.parse(r.rewardValue)
|
||||
return sum + (obj.score || 0)
|
||||
} catch {
|
||||
return sum
|
||||
}
|
||||
}, 0)
|
||||
})
|
||||
|
||||
const totalCount = computed(() => total.value)
|
||||
|
||||
const getTypeColor = (type: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
score: 'green',
|
||||
badge: 'gold',
|
||||
title: 'purple',
|
||||
exp: 'blue'
|
||||
}
|
||||
return colorMap[type] || 'default'
|
||||
}
|
||||
|
||||
const getTypeName = (type: string) => {
|
||||
const nameMap: Record<string, string> = {
|
||||
score: '积分',
|
||||
badge: '徽章',
|
||||
title: '称号',
|
||||
exp: '经验值'
|
||||
}
|
||||
return nameMap[type] || type
|
||||
}
|
||||
|
||||
const parseRewardValue = (value: string) => {
|
||||
try {
|
||||
const obj = JSON.parse(value)
|
||||
if (obj.score) return `+${obj.score} 积分`
|
||||
if (obj.badge) return `徽章: ${obj.badge}`
|
||||
if (obj.title) return `称号: ${obj.title}`
|
||||
return value
|
||||
} catch {
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
const formatTime = (time: string) => {
|
||||
return dayjs(time).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
currentPage.value = page
|
||||
loadData()
|
||||
}
|
||||
|
||||
const loadData = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const result = await getMyRewardRecords({
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
records.value = result.records || []
|
||||
total.value = result.total || 0
|
||||
} catch (error) {
|
||||
console.error('加载奖励记录失败:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
@import "@/styles/variables.scss";
|
||||
|
||||
.reward-records-page {
|
||||
min-height: 100vh;
|
||||
padding: 24px;
|
||||
background: $bg-primary;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.back-btn {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.page-title {
|
||||
font-size: 28px;
|
||||
font-weight: 600;
|
||||
color: $text-primary;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.page-desc {
|
||||
color: $text-secondary;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.stats-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
text-align: center;
|
||||
|
||||
.stat-value {
|
||||
font-size: 36px;
|
||||
font-weight: 700;
|
||||
color: $brand-primary;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: 14px;
|
||||
color: $text-secondary;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.records-section {
|
||||
background: $bg-card;
|
||||
border: 1px solid $border-primary;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.reward-value {
|
||||
color: $brand-primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
:deep(.ant-table) {
|
||||
background: transparent;
|
||||
|
||||
.ant-table-thead > tr > th {
|
||||
background: $bg-secondary;
|
||||
border-bottom: 1px solid $border-primary;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr > td {
|
||||
border-bottom: 1px solid $border-primary;
|
||||
color: $text-primary;
|
||||
}
|
||||
|
||||
.ant-table-tbody > tr:hover > td {
|
||||
background: $bg-secondary;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user