Files
text-adventure-game/components/panels/StatusPanel.vue
Claude 5d4371ba1f feat: 完善数学模型和品质系统
- 修复品质等级范围重叠问题(优秀/稀有无重叠)
- 统一 formulas.js 与 constants.js 的品质判定
- 修复经验公式与游戏实际逻辑不一致
- 调整品质生成概率: 传说0.1%, 史诗1%, 稀有4%, 优秀10%
- 添加数学模型文档和README
- 添加数值验证脚本

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 14:54:20 +08:00

1047 lines
27 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>
<view class="status-panel">
<!-- 资源进度条 -->
<view class="status-panel__resources">
<view class="resource-row">
<text class="resource-label">HP</text>
<view class="resource-bar">
<ProgressBar
:value="player.currentStats.health"
:max="player.currentStats.maxHealth"
color="#ff6b6b"
:showText="true"
height="24rpx"
/>
</view>
</view>
<view class="resource-row">
<text class="resource-label">耐力</text>
<view class="resource-bar">
<ProgressBar
:value="player.currentStats.stamina"
:max="player.currentStats.maxStamina"
color="#ffe66d"
:showText="true"
height="24rpx"
/>
</view>
</view>
<view class="resource-row">
<text class="resource-label">精神</text>
<view class="resource-bar">
<ProgressBar
:value="player.currentStats.sanity"
:max="player.currentStats.maxSanity"
color="#4ecdc4"
:showText="true"
height="24rpx"
/>
</view>
</view>
</view>
<!-- 主属性摘要 -->
<view class="status-panel__stats">
<StatItem label="力量" :value="player.baseStats.strength" icon="💪" />
<StatItem label="敏捷" :value="player.baseStats.agility" icon="🏃" />
<StatItem label="灵巧" :value="player.baseStats.dexterity" icon="🎯" />
<StatItem label="智力" :value="player.baseStats.intuition" icon="🧠" />
<StatItem label="体质" :value="player.baseStats.vitality" icon="❤️" />
</view>
<!-- 等级信息 -->
<view class="status-panel__level">
<text class="level-text">Lv.{{ player.level.current }}</text>
<ProgressBar
:value="player.level.exp"
:max="player.level.maxExp"
color="#a855f7"
:showText="true"
height="16rpx"
/>
</view>
<!-- 货币 -->
<view class="status-panel__currency">
<text class="currency-icon">💰</text>
<text class="currency-text">{{ currencyText }}</text>
</view>
<!-- 装备栏 -->
<view class="status-panel__equipment">
<text class="section-title">🎽 装备</text>
<view class="equipment-slots">
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.weapon }">
<text class="equipment-slot__label">武器</text>
<view v-if="player.equipment.weapon" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.weapon.qualityColor }">
{{ player.equipment.weapon.icon }} {{ player.equipment.weapon.name }}
</text>
<text class="equipment-slot__stats">{{ player.equipment.weapon.finalDamage }}</text>
<text class="equipment-slot__quality">[{{ player.equipment.weapon.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.armor }">
<text class="equipment-slot__label">防具</text>
<view v-if="player.equipment.armor" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.armor.qualityColor }">
{{ player.equipment.armor.icon }} {{ player.equipment.armor.name }}
</text>
<text class="equipment-slot__stats">{{ player.equipment.armor.finalDefense }}</text>
<text class="equipment-slot__quality">[{{ player.equipment.armor.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.shield }">
<text class="equipment-slot__label">盾牌</text>
<view v-if="player.equipment.shield" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.shield.qualityColor }">
{{ player.equipment.shield.icon }} {{ player.equipment.shield.name }}
</text>
<text class="equipment-slot__stats">格挡{{ player.equipment.shield.finalShield }}</text>
<text class="equipment-slot__quality">[{{ player.equipment.shield.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
<view class="equipment-slot" :class="{ 'equipment-slot--empty': !player.equipment.accessory }">
<text class="equipment-slot__label">饰品</text>
<view v-if="player.equipment.accessory" class="equipment-slot__item">
<text class="equipment-slot__name" :style="{ color: player.equipment.accessory.qualityColor }">
{{ player.equipment.accessory.icon }} {{ player.equipment.accessory.name }}
</text>
<text class="equipment-slot__quality">[{{ player.equipment.accessory.qualityName }}]</text>
</view>
<text v-else class="equipment-slot__empty"></text>
</view>
</view>
</view>
<!-- 更多属性折叠 -->
<Collapse
title="更多属性"
:expanded="showMoreStats"
@toggle="showMoreStats = $event"
>
<view class="status-panel__more-stats">
<StatItem label="攻击力" :value="finalStats.attack" />
<StatItem label="防御力" :value="finalStats.defense" />
<StatItem label="暴击率" :value="finalStats.critRate + '%'" />
<StatItem label="AP" :value="finalStats.ap" />
<StatItem label="EP" :value="finalStats.ep" />
</view>
</Collapse>
<!-- 技能筛选 -->
<FilterTabs
:tabs="skillFilterTabs"
:modelValue="currentSkillFilter"
@update:modelValue="currentSkillFilter = $event"
/>
<!-- 技能列表 -->
<scroll-view class="status-panel__skills" scroll-y>
<view v-for="skill in filteredSkills" :key="skill.id" class="skill-item">
<view class="skill-item__main" @click="toggleSkillDetail(skill.id)">
<text class="skill-item__icon">{{ skill.icon || '⚡' }}</text>
<view class="skill-item__info">
<text class="skill-item__name">{{ skill.name }}</text>
<text class="skill-item__level">Lv.{{ skill.level }}/{{ skill.maxLevel }}</text>
</view>
<view class="skill-item__bar">
<ProgressBar
:value="skill.exp"
:max="skill.maxExp"
height="8rpx"
:showText="false"
/>
<text class="skill-item__progress">{{ Math.floor(skill.exp) }}/{{ Math.floor(skill.maxExp) }}</text>
</view>
<text class="skill-item__expand">{{ expandedSkills.includes(skill.id) ? '▼' : '▶' }}</text>
</view>
<!-- 技能详情展开后显示 -->
<view v-if="expandedSkills.includes(skill.id)" class="skill-item__detail">
<!-- 已获得的增幅 -->
<view v-if="getActiveMilestones(skill).length > 0" class="skill-detail__section">
<text class="skill-detail__title"> 已获得增幅</text>
<view v-for="m in getActiveMilestones(skill)" :key="m.level" class="skill-detail__bonus">
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
</view>
</view>
<!-- 即将获得的增幅 -->
<view v-if="getUpcomingMilestones(skill).length > 0" class="skill-detail__section">
<text class="skill-detail__title">即将解锁</text>
<view v-for="m in getUpcomingMilestones(skill)" :key="m.level" class="skill-detail__bonus upcoming">
<text class="skill-detail__bonus-level">Lv.{{ m.level }}</text>
<text class="skill-detail__bonus-desc">{{ m.desc }}</text>
</view>
</view>
<view v-if="getActiveMilestones(skill).length === 0 && getUpcomingMilestones(skill).length === 0" class="skill-detail__empty">
<text class="skill-detail__empty-text">暂无增幅效果</text>
</view>
</view>
</view>
<view v-if="filteredSkills.length === 0" class="skill-empty">
<text class="skill-empty__text">暂无技能</text>
</view>
</scroll-view>
<!-- 书籍区域 -->
<Collapse title="📚 书籍" :expanded="showBooks" @toggle="showBooks = $event">
<!-- 活动中的阅读任务 -->
<view v-if="readingTask" class="reading-task">
<text class="reading-task__title">正在阅读{{ readingTask.bookName }}</text>
<ProgressBar
:value="readingTask.progress"
:max="readingTask.totalTime"
color="#4ecdc4"
:showText="true"
height="16rpx"
/>
<text class="reading-task__time">{{ readingTimeText }}</text>
</view>
<!-- 可阅读的书籍列表 -->
<view v-if="!readingTask && ownedBooks.length > 0" class="books-list">
<view
v-for="book in ownedBooks"
:key="book.id"
class="book-item"
@click="startReading(book)"
>
<text class="book-item__icon">{{ book.icon || '📖' }}</text>
<view class="book-item__info">
<text class="book-item__name">{{ book.name }}</text>
<text class="book-item__desc">阅读需 {{ book.readingTime }}</text>
</view>
<TextButton text="阅读" type="primary" size="small" />
</view>
</view>
<view v-if="!readingTask && ownedBooks.length === 0" class="books-empty">
<text class="books-empty__text">暂无书籍去商店看看吧</text>
</view>
</Collapse>
<!-- 训练区域 -->
<Collapse title="⚔️ 训练" :expanded="showTraining" @toggle="showTraining = $event">
<!-- 活动中的训练任务 -->
<view v-if="trainingTask" class="training-task">
<text class="training-task__title">正在训练{{ trainingTask.skillName }}</text>
<ProgressBar
:value="trainingTask.progress"
:max="trainingTask.totalTime || 300"
color="#ff6b6b"
:showText="true"
height="16rpx"
/>
<TextButton text="停止训练" type="warning" size="small" @click="stopTraining" />
</view>
<!-- 可训练的技能列表 -->
<view v-if="!trainingTask && trainableSkills.length > 0" class="trainable-list">
<view
v-for="skill in trainableSkills"
:key="skill.id"
class="trainable-item"
>
<text class="trainable-item__icon">{{ skill.icon || '⚔️' }}</text>
<view class="trainable-item__info">
<text class="trainable-item__name">{{ skill.name }}</text>
<text class="trainable-item__level">Lv.{{ skill.level }}</text>
</view>
<TextButton
text="训练"
type="primary"
size="small"
:disabled="player.currentStats.stamina < 10"
@click="startTraining(skill)"
/>
</view>
</view>
<view v-if="!trainingTask && trainableSkills.length === 0" class="training-empty">
<text class="training-empty__text">暂无可训练技能</text>
</view>
</Collapse>
<!-- 活动任务面板 -->
<Collapse title="📋 活动任务" :expanded="showTasks" @toggle="showTasks = $event">
<view v-if="activeTasksList.length > 0" class="active-tasks-list">
<view v-for="task in activeTasksList" :key="task.id" class="active-task-item">
<view class="active-task-item__header">
<text class="active-task-item__name">{{ task.name }}</text>
<TextButton
text="取消"
type="warning"
size="small"
@click="cancelActiveTask(task)"
/>
</view>
<ProgressBar
:value="task.progress"
:max="task.total || 100"
color="#4ecdc4"
:showText="true"
height="12rpx"
/>
<text class="active-task-item__time">{{ task.timeText }}</text>
</view>
</view>
<view v-else class="tasks-empty">
<text class="tasks-empty__text">暂无活动任务</text>
</view>
</Collapse>
</view>
</template>
<script setup>
import { computed, ref } from 'vue'
import { usePlayerStore } from '@/store/player'
import { useGameStore } from '@/store/game'
import { SKILL_CONFIG } from '@/config/skills'
import { ITEM_CONFIG } from '@/config/items'
import { startTask, endTask } from '@/utils/taskSystem'
import { getSkillDisplayInfo } from '@/utils/skillSystem'
import ProgressBar from '@/components/common/ProgressBar.vue'
import StatItem from '@/components/common/StatItem.vue'
import Collapse from '@/components/common/Collapse.vue'
import FilterTabs from '@/components/common/FilterTabs.vue'
import TextButton from '@/components/common/TextButton.vue'
const player = usePlayerStore()
const game = useGameStore()
const showMoreStats = ref(false)
const currentSkillFilter = ref('all')
const showBooks = ref(false)
const showTraining = ref(false)
const showTasks = ref(true)
const expandedSkills = ref([]) // 展开的技能ID列表
const skillFilterTabs = [
{ id: 'all', label: '全部' },
{ id: 'combat', label: '战斗' },
{ id: 'life', label: '生活' },
{ id: 'passive', label: '被动' }
]
const currencyText = computed(() => {
const c = player.currency.copper
const gold = Math.floor(c / 10000)
const silver = Math.floor((c % 10000) / 100)
const copper = c % 100
const parts = []
if (gold > 0) parts.push(`${gold}`)
if (silver > 0 || gold > 0) parts.push(`${silver}`)
parts.push(`${copper}`)
return parts.join(' ')
})
const filteredSkills = computed(() => {
const skills = Object.entries(player.skills || {})
.filter(([_, skill]) => skill.unlocked)
.map(([id, _]) => getSkillDisplayInfo(player, id))
.filter(Boolean)
.sort((a, b) => b.level - a.level || b.exp - a.exp)
if (currentSkillFilter.value === 'all') {
return skills
}
return skills.filter(s => s.type === currentSkillFilter.value)
})
const finalStats = computed(() => {
// 计算最终属性(基础值 + 装备加成 + 全局加成)
let attack = player.baseStats?.strength || 0
let defense = 0
let critRate = (player.baseStats?.dexterity || 0) + (player.globalBonus?.critRate || 0)
// 装备属性加成
if (player.equipmentStats) {
attack += player.equipmentStats.attack || 0
defense += player.equipmentStats.defense || 0
critRate += player.equipmentStats.critRate || 0
}
// 如果没有equipmentStats直接从装备计算
if (!player.equipmentStats) {
// 武器攻击力使用finalDamage如果存在
if (player.equipment.weapon) {
const weapon = player.equipment.weapon
attack += weapon.finalDamage || weapon.baseDamage || 0
}
// 防具防御力使用finalDefense如果存在
if (player.equipment.armor) {
const armor = player.equipment.armor
defense += armor.finalDefense || armor.baseDefense || 0
}
// 盾牌格挡
if (player.equipment.shield) {
const shield = player.equipment.shield
defense += shield.finalDefense || shield.baseDefense || 0
}
}
// 计算AP和EP包含装备加成
const ap = (player.baseStats?.dexterity || 0) + (player.baseStats?.intuition || 0) * 0.2
const ep = (player.baseStats?.agility || 0) + (player.baseStats?.intuition || 0) * 0.2
return {
attack: Math.floor(attack),
defense: Math.floor(defense),
critRate: Math.min(80, Math.floor(critRate)),
ap: Math.floor(ap),
ep: Math.floor(ep)
}
})
// 书籍相关
const ownedBooks = computed(() => {
return (player.inventory || []).filter(item => item.type === 'book')
})
const readingTask = computed(() => {
const task = (game.activeTasks || []).find(t => t.type === 'reading')
if (!task) return null
const bookConfig = ITEM_CONFIG[task.data.itemId]
return {
bookName: bookConfig?.name || '未知书籍',
progress: task.progress,
totalTime: bookConfig?.readingTime || 60
}
})
const readingTimeText = computed(() => {
if (!readingTask.value) return ''
const remaining = Math.max(0, readingTask.value.totalTime - readingTask.value.progress)
const minutes = Math.floor(remaining / 60)
const seconds = Math.floor(remaining % 60)
return `剩余 ${minutes}:${String(seconds).padStart(2, '0')}`
})
// 技能详情相关函数
function toggleSkillDetail(skillId) {
const index = expandedSkills.value.indexOf(skillId)
if (index > -1) {
expandedSkills.value.splice(index, 1)
} else {
expandedSkills.value.push(skillId)
}
}
function getActiveMilestones(skill) {
const config = SKILL_CONFIG[skill.id]
if (!config || !config.milestones) return []
return Object.entries(config.milestones)
.filter(([level]) => parseInt(level) <= skill.level)
.map(([level, milestone]) => ({
level: parseInt(level),
desc: milestone.desc
}))
.sort((a, b) => a.level - b.level)
}
function getUpcomingMilestones(skill) {
const config = SKILL_CONFIG[skill.id]
if (!config || !config.milestones) return []
return Object.entries(config.milestones)
.filter(([level]) => parseInt(level) > skill.level)
.map(([level, milestone]) => ({
level: parseInt(level),
desc: milestone.desc
}))
.sort((a, b) => a.level - b.level)
}
function startReading(book) {
const result = startTask(game, player, 'reading', {
itemId: book.id,
duration: book.readingTime || 60
})
if (result.success) {
game.addLog(`开始阅读《${book.name}`, 'info')
} else {
game.addLog(result.message, 'error')
}
}
// 训练相关
const trainableSkills = computed(() => {
return Object.entries(player.skills || {})
.filter(([_, skill]) => skill.unlocked && SKILL_CONFIG[_]?.type === 'combat')
.map(([id, _]) => {
const info = getSkillDisplayInfo(player, id)
return {
id,
name: info?.name || id,
icon: info?.icon || '⚔️',
level: info?.level || 0
}
})
.filter(Boolean)
})
const trainingTask = computed(() => {
const task = (game.activeTasks || []).find(t => t.type === 'training')
if (!task) return null
const info = getSkillDisplayInfo(player, task.data.skillId)
return {
skillName: info?.name || '未知技能',
progress: task.progress,
totalTime: task.data.duration || 300,
taskId: task.id
}
})
function startTraining(skill) {
const result = startTask(game, player, 'training', {
skillId: skill.id,
duration: 60 // 1分钟
})
if (result.success) {
game.addLog(`开始训练 ${skill.name}`, 'info')
} else {
game.addLog(result.message, 'error')
}
}
function stopTraining() {
if (trainingTask.value) {
endTask(game, player, trainingTask.value.taskId, false)
game.addLog('停止了训练', 'info')
}
}
// 活动任务相关
const activeTasksList = computed(() => {
const tasks = (game.activeTasks || []).map(task => {
let name = '未知任务'
let total = 100
if (task.type === 'reading') {
const book = ITEM_CONFIG[task.data.itemId]
name = `阅读《${book?.name || '未知书籍'}`
total = book?.readingTime || 60
} else if (task.type === 'training') {
const skill = SKILL_CONFIG[task.data.skillId]
name = `训练 ${skill?.name || '技能'}`
total = task.data.duration || 300
} else if (task.type === 'resting') {
name = '休息'
total = task.data.duration || 60
}
const progress = Math.min(task.progress, total)
const percentage = Math.floor((progress / total) * 100)
// 计算剩余时间
const remaining = Math.max(0, total - progress)
const minutes = Math.floor(remaining / 60)
const seconds = Math.floor(remaining % 60)
let timeText = ''
if (minutes > 0) {
timeText = `剩余 ${minutes}${seconds}`
} else {
timeText = `剩余 ${seconds}`
}
return {
id: task.id,
name,
progress,
total,
percentage,
timeText
}
})
return tasks.sort((a, b) => b.progress - a.progress)
})
function cancelActiveTask(task) {
const { endTask } = require('@/utils/taskSystem')
endTask(game, player, task.id, false)
game.addLog(`取消了 ${task.name}`, 'info')
}
</script>
<style lang="scss" scoped>
.status-panel {
display: flex;
flex-direction: column;
height: 100%;
padding: 16rpx;
background-color: $bg-primary;
&__resources {
display: flex;
flex-direction: column;
gap: 12rpx;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__stats {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__level {
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__currency {
display: flex;
align-items: center;
gap: 12rpx;
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__equipment {
padding: 16rpx;
background-color: $bg-secondary;
border-radius: 12rpx;
margin-bottom: 16rpx;
}
&__more-stats {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
}
&__skills {
flex: 1;
margin-top: 16rpx;
}
}
.resource-row {
display: flex;
align-items: center;
gap: 12rpx;
}
.resource-bar {
flex: 1;
min-width: 0;
}
.resource-label {
min-width: 80rpx;
color: $text-secondary;
font-size: 24rpx;
}
.level-text {
display: block;
color: $accent;
font-size: 28rpx;
font-weight: bold;
margin-bottom: 8rpx;
}
.currency-icon {
font-size: 32rpx;
}
.currency-text {
color: $warning;
font-size: 28rpx;
font-weight: bold;
}
.skill-item {
display: flex;
flex-direction: column;
background-color: $bg-secondary;
border-radius: 8rpx;
margin-bottom: 8rpx;
overflow: hidden;
&__main {
display: flex;
align-items: center;
gap: 12rpx;
padding: 12rpx;
cursor: pointer;
}
&__icon {
font-size: 32rpx;
width: 48rpx;
text-align: center;
}
&__info {
flex: 1;
display: flex;
justify-content: space-between;
align-items: center;
}
&__name {
color: $text-primary;
font-size: 26rpx;
}
&__level {
color: $accent;
font-size: 24rpx;
}
&__bar {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4rpx;
}
&__progress {
color: $text-muted;
font-size: 20rpx;
text-align: right;
}
&__expand {
color: $text-muted;
font-size: 20rpx;
padding-left: 8rpx;
}
&__detail {
padding: 12rpx;
padding-top: 0;
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
}
.skill-detail {
&__section {
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
}
&__title {
color: $text-primary;
font-size: 24rpx;
font-weight: bold;
margin-bottom: 8rpx;
display: block;
}
&__bonus {
display: flex;
align-items: center;
gap: 12rpx;
padding: 6rpx 12rpx;
background-color: rgba(78, 205, 196, 0.1);
border-radius: 4rpx;
margin-bottom: 6rpx;
&.upcoming {
background-color: rgba(255, 107, 107, 0.1);
}
}
&__bonus-level {
color: $accent;
font-size: 22rpx;
font-weight: bold;
min-width: 60rpx;
}
&__bonus-desc {
color: $text-secondary;
font-size: 22rpx;
}
&__empty {
padding: 16rpx;
text-align: center;
}
&__empty-text {
color: $text-muted;
font-size: 24rpx;
}
}
.skill-empty {
padding: 48rpx;
text-align: center;
&__text {
color: $text-muted;
font-size: 28rpx;
}
}
.section-title {
display: block;
color: $text-primary;
font-size: 24rpx;
font-weight: bold;
margin-bottom: 12rpx;
}
.equipment-slots {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8rpx;
}
.equipment-slot {
display: flex;
flex-direction: column;
align-items: center;
padding: 12rpx;
background-color: $bg-primary;
border-radius: 8rpx;
border: 1rpx solid $border-color;
&--empty {
opacity: 0.6;
}
&__label {
color: $text-secondary;
font-size: 20rpx;
margin-bottom: 4rpx;
}
&__item {
display: flex;
flex-direction: column;
align-items: center;
gap: 2rpx;
}
&__name {
color: $text-primary;
font-size: 22rpx;
text-align: center;
}
&__stats {
color: $accent;
font-size: 20rpx;
}
&__quality {
color: $text-muted;
font-size: 18rpx;
}
&__empty {
color: $text-muted;
font-size: 22rpx;
}
}
// 书籍相关样式
.reading-task {
padding: 16rpx;
background-color: $bg-tertiary;
border-radius: 8rpx;
margin-bottom: 12rpx;
&__title {
display: block;
color: $text-primary;
font-size: 24rpx;
margin-bottom: 8rpx;
}
&__time {
display: block;
color: $text-secondary;
font-size: 20rpx;
margin-top: 8rpx;
text-align: right;
}
}
.books-list {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.book-item {
display: flex;
align-items: center;
gap: 12rpx;
padding: 12rpx;
background-color: $bg-primary;
border-radius: 8rpx;
&:active {
background-color: $bg-tertiary;
}
&__icon {
font-size: 32rpx;
}
&__info {
flex: 1;
}
&__name {
display: block;
color: $text-primary;
font-size: 26rpx;
}
&__desc {
display: block;
color: $text-secondary;
font-size: 22rpx;
margin-top: 4rpx;
}
}
.books-empty {
padding: 32rpx;
text-align: center;
&__text {
color: $text-muted;
font-size: 24rpx;
}
}
// 训练相关样式
.training-task {
padding: 16rpx;
background-color: $bg-tertiary;
border-radius: 8rpx;
margin-bottom: 12rpx;
&__title {
display: block;
color: $text-primary;
font-size: 24rpx;
margin-bottom: 8rpx;
}
}
.trainable-list {
display: flex;
flex-direction: column;
gap: 8rpx;
}
.trainable-item {
display: flex;
align-items: center;
gap: 12rpx;
padding: 12rpx;
background-color: $bg-primary;
border-radius: 8rpx;
&__icon {
font-size: 32rpx;
}
&__info {
flex: 1;
}
&__name {
display: block;
color: $text-primary;
font-size: 26rpx;
}
&__level {
display: block;
color: $accent;
font-size: 22rpx;
margin-top: 4rpx;
}
}
.training-empty {
padding: 32rpx;
text-align: center;
&__text {
color: $text-muted;
font-size: 24rpx;
}
}
// 活动任务样式
.active-tasks-list {
display: flex;
flex-direction: column;
gap: 12rpx;
}
.active-task-item {
padding: 12rpx;
background-color: $bg-primary;
border-radius: 8rpx;
&__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8rpx;
}
&__name {
color: $text-primary;
font-size: 24rpx;
}
&__time {
display: block;
color: $text-secondary;
font-size: 20rpx;
margin-top: 4rpx;
}
}
.tasks-empty {
padding: 32rpx;
text-align: center;
&__text {
color: $text-muted;
font-size: 24rpx;
}
}
</style>