2026-01-21 17:13:51 +08:00
|
|
|
|
<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>
|
2026-01-25 14:54:20 +08:00
|
|
|
|
<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>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
<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>
|
2026-01-25 14:54:20 +08:00
|
|
|
|
<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>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
<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>
|
2026-01-25 14:54:20 +08:00
|
|
|
|
<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>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
<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>
|
2026-01-25 14:54:20 +08:00
|
|
|
|
<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>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
<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">
|
2026-01-25 14:54:20 +08:00
|
|
|
|
<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>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
</view>
|
2026-01-25 14:54:20 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 技能详情(展开后显示) -->
|
|
|
|
|
|
<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>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
<view v-if="filteredSkills.length === 0" class="skill-empty">
|
|
|
|
|
|
<text class="skill-empty__text">暂无技能</text>
|
|
|
|
|
|
</view>
|
|
|
|
|
|
</scroll-view>
|
2026-01-23 16:20:10 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 书籍区域 -->
|
|
|
|
|
|
<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>
|
2026-01-21 17:13:51 +08:00
|
|
|
|
</view>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { computed, ref } from 'vue'
|
|
|
|
|
|
import { usePlayerStore } from '@/store/player'
|
2026-01-23 16:20:10 +08:00
|
|
|
|
import { useGameStore } from '@/store/game'
|
2026-01-21 17:13:51 +08:00
|
|
|
|
import { SKILL_CONFIG } from '@/config/skills'
|
2026-01-23 16:20:10 +08:00
|
|
|
|
import { ITEM_CONFIG } from '@/config/items'
|
2026-01-23 19:40:55 +08:00
|
|
|
|
import { startTask, endTask } from '@/utils/taskSystem'
|
2026-01-23 16:20:10 +08:00
|
|
|
|
import { getSkillDisplayInfo } from '@/utils/skillSystem'
|
2026-01-21 17:13:51 +08:00
|
|
|
|
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'
|
2026-01-23 16:20:10 +08:00
|
|
|
|
import TextButton from '@/components/common/TextButton.vue'
|
2026-01-21 17:13:51 +08:00
|
|
|
|
|
|
|
|
|
|
const player = usePlayerStore()
|
2026-01-23 16:20:10 +08:00
|
|
|
|
const game = useGameStore()
|
2026-01-21 17:13:51 +08:00
|
|
|
|
const showMoreStats = ref(false)
|
|
|
|
|
|
const currentSkillFilter = ref('all')
|
2026-01-23 16:20:10 +08:00
|
|
|
|
const showBooks = ref(false)
|
|
|
|
|
|
const showTraining = ref(false)
|
|
|
|
|
|
const showTasks = ref(true)
|
2026-01-25 14:54:20 +08:00
|
|
|
|
const expandedSkills = ref([]) // 展开的技能ID列表
|
2026-01-21 17:13:51 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-01-23 16:20:10 +08:00
|
|
|
|
.map(([id, _]) => getSkillDisplayInfo(player, id))
|
|
|
|
|
|
.filter(Boolean)
|
2026-01-21 17:13:51 +08:00
|
|
|
|
.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(() => {
|
2026-01-23 16:20:10 +08:00
|
|
|
|
// 计算最终属性(基础值 + 装备加成 + 全局加成)
|
|
|
|
|
|
let attack = player.baseStats?.strength || 0
|
2026-01-21 17:13:51 +08:00
|
|
|
|
let defense = 0
|
2026-01-23 16:20:10 +08:00
|
|
|
|
let critRate = (player.baseStats?.dexterity || 0) + (player.globalBonus?.critRate || 0)
|
2026-01-21 17:13:51 +08:00
|
|
|
|
|
2026-01-23 16:20:10 +08:00
|
|
|
|
// 装备属性加成
|
|
|
|
|
|
if (player.equipmentStats) {
|
|
|
|
|
|
attack += player.equipmentStats.attack || 0
|
|
|
|
|
|
defense += player.equipmentStats.defense || 0
|
|
|
|
|
|
critRate += player.equipmentStats.critRate || 0
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:20:10 +08:00
|
|
|
|
// 如果没有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
|
|
|
|
|
|
}
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:20:10 +08:00
|
|
|
|
// 计算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
|
2026-01-21 17:13:51 +08:00
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
|
attack: Math.floor(attack),
|
|
|
|
|
|
defense: Math.floor(defense),
|
2026-01-23 16:20:10 +08:00
|
|
|
|
critRate: Math.min(80, Math.floor(critRate)),
|
2026-01-21 17:13:51 +08:00
|
|
|
|
ap: Math.floor(ap),
|
|
|
|
|
|
ep: Math.floor(ep)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
2026-01-23 16:20:10 +08:00
|
|
|
|
|
|
|
|
|
|
// 书籍相关
|
|
|
|
|
|
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')}`
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-01-25 14:54:20 +08:00
|
|
|
|
// 技能详情相关函数
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-01-23 16:20:10 +08:00
|
|
|
|
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,
|
2026-01-23 19:40:55 +08:00
|
|
|
|
duration: 60 // 1分钟
|
2026-01-23 16:20:10 +08:00
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
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')
|
|
|
|
|
|
}
|
2026-01-21 17:13:51 +08:00
|
|
|
|
</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;
|
2026-01-25 14:54:20 +08:00
|
|
|
|
flex-direction: column;
|
2026-01-21 17:13:51 +08:00
|
|
|
|
background-color: $bg-secondary;
|
|
|
|
|
|
border-radius: 8rpx;
|
|
|
|
|
|
margin-bottom: 8rpx;
|
2026-01-25 14:54:20 +08:00
|
|
|
|
overflow: hidden;
|
|
|
|
|
|
|
|
|
|
|
|
&__main {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 12rpx;
|
|
|
|
|
|
padding: 12rpx;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
}
|
2026-01-21 17:13:51 +08:00
|
|
|
|
|
|
|
|
|
|
&__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;
|
2026-01-23 19:40:55 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
gap: 4rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__progress {
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
text-align: right;
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
2026-01-25 14:54:20 +08:00
|
|
|
|
|
|
|
|
|
|
&__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;
|
|
|
|
|
|
}
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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 {
|
2026-01-25 14:54:20 +08:00
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
gap: 2rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__name {
|
2026-01-21 17:13:51 +08:00
|
|
|
|
color: $text-primary;
|
|
|
|
|
|
font-size: 22rpx;
|
2026-01-25 14:54:20 +08:00
|
|
|
|
text-align: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__stats {
|
|
|
|
|
|
color: $accent;
|
|
|
|
|
|
font-size: 20rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__quality {
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
font-size: 18rpx;
|
2026-01-21 17:13:51 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
&__empty {
|
|
|
|
|
|
color: $text-muted;
|
|
|
|
|
|
font-size: 22rpx;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-23 16:20:10 +08:00
|
|
|
|
|
|
|
|
|
|
// 书籍相关样式
|
|
|
|
|
|
.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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-01-21 17:13:51 +08:00
|
|
|
|
</style>
|