Files
text-adventure-game/components/drawers/CraftingDrawer.vue
Claude 021f6a54f5 fix: 修复制造系统耦合问题并优化物品堆叠逻辑
- 修复 craftingSystem.js 引用不存在的 inventorySystem.js
- 移除 recipes.js 中重复的 getItemCount 函数
- 装备类物品改为按品质等级(1-6)分组堆叠,而非精确品质值
- 统一品质计算,使用 GAME_CONSTANTS.QUALITY_LEVELS 范围
- 移除未使用的 createCraftedItem 函数

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 16:19:17 +08:00

669 lines
15 KiB
Vue
Raw Permalink 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="crafting-drawer">
<view class="drawer-header">
<view class="header-left">
<text class="drawer-title">🔨 制造</text>
<text class="drawer-subtitle">{{ getSelectedCategoryName() }}</text>
</view>
<text class="drawer-close" @click="close">×</text>
</view>
<!-- 技能信息显示 -->
<view class="crafting-skill-info">
<text class="skill-info__label">制造技能等级:</text>
<text class="skill-info__level">{{ getCraftingSkillLevel() }}</text>
</view>
<!-- 分类标签 -->
<FilterTabs
:tabs="categories"
:modelValue="selectedCategory"
@update:modelValue="selectedCategory = $event"
/>
<!-- 配方列表 -->
<scroll-view class="recipe-list" scroll-y>
<view
v-for="recipe in filteredRecipes"
:key="recipe.id"
class="recipe-item"
:class="{ 'recipe-item--disabled': !recipe.canCraftNow }"
@click="selectRecipe(recipe)"
>
<view class="recipe-item__main">
<text class="recipe-item__icon">{{ recipe.displayIcon || '📦' }}</text>
<view class="recipe-item__info">
<text class="recipe-item__name">{{ recipe.displayName }}</text>
<text class="recipe-item__desc">{{ getRecipeDescription(recipe) }}</text>
<!-- 材料需求 -->
<view class="recipe-materials">
<text
v-for="mat in recipe.materials"
:key="mat.itemId"
class="material-tag"
:class="{ 'material-tag--missing': isMaterialMissing(recipe, mat.itemId) }"
>
{{ getMaterialIcon(mat.itemId) }} {{ getMaterialName(mat.itemId) }} x{{ mat.count }}
</text>
</view>
</view>
</view>
<view class="recipe-item__right">
<text class="recipe-time">{{ getCraftingTimeDisplay(recipe) }}</text>
<text class="recipe-success-rate" :class="getSuccessRateClass(recipe)">
{{ getSuccessRateDisplay(recipe) }}
</text>
</view>
</view>
<view v-if="filteredRecipes.length === 0" class="recipe-empty">
<text class="recipe-empty__text">此分类下没有可制作的配方</text>
</view>
</scroll-view>
<!-- 当前制造进度 -->
<view v-if="activeCraftingTask" class="crafting-progress">
<view class="progress-header">
<text class="progress-title">正在制造...</text>
<text class="progress-percent">{{ getCraftingProgress() }}%</text>
</view>
<view class="progress-bar">
<view class="progress-bar__fill" :style="{ width: getCraftingProgress() + '%' }"></view>
</view>
<text class="progress-item">{{ getCraftingItemName() }}</text>
</view>
<!-- 配方详情弹窗 -->
<view v-if="selectedRecipe" class="recipe-detail-popup" @click="selectedRecipe = null">
<view class="recipe-detail" @click.stop>
<view class="recipe-detail__header">
<text class="recipe-detail__icon">{{ selectedRecipe.displayIcon || '📦' }}</text>
<text class="recipe-detail__name">{{ selectedRecipe.displayName }}</text>
</view>
<!-- 配方属性 -->
<view class="recipe-detail__stats">
<view class="stat-row">
<text class="stat-label">制造时间:</text>
<text class="stat-value">{{ getCraftingTimeDisplay(selectedRecipe) }}</text>
</view>
<view class="stat-row">
<text class="stat-label">成功率:</text>
<text class="stat-value" :class="getSuccessRateClass(selectedRecipe)">
{{ getSuccessRateDisplay(selectedRecipe) }}
</text>
</view>
<view class="stat-row">
<text class="stat-label">所需技能:</text>
<text class="stat-value">{{ getRequiredSkillDisplay(selectedRecipe) }}</text>
</view>
</view>
<!-- 材料列表 -->
<view class="recipe-detail__materials">
<text class="materials-title">所需材料:</text>
<view class="materials-list">
<view
v-for="mat in selectedRecipe.materials"
:key="mat.itemId"
class="material-item"
:class="{ 'material-item--missing': isMaterialMissing(selectedRecipe, mat.itemId) }"
>
<text class="material-item__icon">{{ getMaterialIcon(mat.itemId) }}</text>
<text class="material-item__name">{{ getMaterialName(mat.itemId) }}</text>
<text class="material-item__count">{{ mat.count }}</text>
<text class="material-item__have">拥有: {{ getMaterialCount(mat.itemId) }}</text>
</view>
</view>
</view>
<!-- 预估品质 -->
<view class="recipe-detail__quality">
<text class="quality-title">预估品质范围:</text>
<view class="quality-range">
<text class="quality-range__min" :style="{ color: getQualityColor(2) }">普通</text>
<text class="quality-range__arrow"></text>
<text class="quality-range__max" :style="{ color: getQualityColor(5) }">史诗</text>
</view>
</view>
<!-- 操作按钮 -->
<view class="recipe-detail__actions">
<TextButton
text="开始制造"
type="primary"
:disabled="!selectedRecipe.canCraftNow"
@click="startCraftingRecipe"
/>
<TextButton text="取消" @click="selectedRecipe = null" />
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue'
import { usePlayerStore } from '@/store/player'
import { useGameStore } from '@/store/game'
import { ITEM_CONFIG } from '@/config/items.js'
import {
getCraftableRecipes,
getRecipesByCategory,
calculateCraftingTime,
calculateSuccessRate,
startCrafting,
completeCrafting,
getAllCategories,
getCategoryInfo
} from '@/utils/craftingSystem.js'
import { getActiveTasks } from '@/utils/taskSystem.js'
import { TASK_TYPES } from '@/utils/taskSystem.js'
import FilterTabs from '@/components/common/FilterTabs.vue'
import TextButton from '@/components/common/TextButton.vue'
const player = usePlayerStore()
const game = useGameStore()
const selectedCategory = ref('weapon')
const selectedRecipe = ref(null)
// 分类列表
const categories = computed(() => {
const allCats = getAllCategories()
return allCats.map(cat => ({
id: cat.id,
name: cat.name,
icon: cat.icon
}))
})
// 可制作的配方
const craftableRecipes = computed(() => {
return getCraftableRecipes(player)
})
// 当前分类的配方
const filteredRecipes = computed(() => {
return getRecipesByCategory(player, selectedCategory.value)
})
// 活动中的制造任务
const activeCraftingTask = computed(() => {
const activeTasks = getActiveTasks(game)
return activeTasks.find(t => t.type === TASK_TYPES.CRAFTING)
})
// 获取选中分类名称
function getSelectedCategoryName() {
const cat = getCategoryInfo(selectedCategory.value)
return cat.name
}
// 获取制造技能等级
function getCraftingSkillLevel() {
const craftingSkill = player.skills.crafting
if (!craftingSkill || !craftingSkill.unlocked) {
return '未解锁'
}
return `Lv.${craftingSkill.level}`
}
// 获取配方描述
function getRecipeDescription(recipe) {
const item = ITEM_CONFIG[recipe.resultItem]
return item?.description || ''
}
// 检查材料是否缺失
function isMaterialMissing(recipe, materialId) {
const missing = recipe.missingMaterials?.find(m => m.itemId === materialId)
return !!missing
}
// 获取材料图标
function getMaterialIcon(itemId) {
const item = ITEM_CONFIG[itemId]
return item?.icon || '📦'
}
// 获取材料名称
function getMaterialName(itemId) {
const item = ITEM_CONFIG[itemId]
return item?.name || itemId
}
// 获取材料拥有数量
function getMaterialCount(itemId) {
const invItem = player.inventory.find(i => i.id === itemId)
return invItem?.count || 0
}
// 获取制造时间显示
function getCraftingTimeDisplay(recipe) {
const time = calculateCraftingTime(player, recipe)
if (time >= 60) {
return `${Math.floor(time / 60)}${time % 60}`
}
return `${time}`
}
// 获取成功率显示
function getSuccessRateDisplay(recipe) {
const rate = calculateSuccessRate(player, recipe)
return `${rate}%`
}
// 获取成功率样式类
function getSuccessRateClass(recipe) {
const rate = calculateSuccessRate(player, recipe)
if (rate >= 80) return 'success-rate--high'
if (rate >= 50) return 'success-rate--medium'
return 'success-rate--low'
}
// 获取所需技能显示
function getRequiredSkillDisplay(recipe) {
const skill = player.skills[recipe.requiredSkill]
if (!skill || !skill.unlocked) {
return `需要 ${recipe.requiredSkill} Lv.${recipe.requiredSkillLevel}`
}
if (skill.level < recipe.requiredSkillLevel) {
return `${recipe.requiredSkill} Lv.${skill.level}/${recipe.requiredSkillLevel}`
}
return `${recipe.requiredSkill} Lv.${skill.level}`
}
// 获取品质颜色
function getQualityColor(quality) {
const colors = {
1: '#9e9e9e', // 垃圾
2: '#ffffff', // 普通
3: '#4caf50', // 优秀
4: '#2196f3', // 稀有
5: '#9c27b0', // 史诗
6: '#ff9800' // 传说
}
return colors[quality] || '#ffffff'
}
// 获取制造进度
function getCraftingProgress() {
if (!activeCraftingTask.value) return 0
return Math.floor(activeCraftingTask.value.progress.percentage || 0)
}
// 获取制造物品名称
function getCraftingItemName() {
if (!activeCraftingTask.value) return ''
const recipeId = activeCraftingTask.value.data.recipeId
const item = ITEM_CONFIG[recipeId]
return item?.name || recipeId
}
// 选择配方
function selectRecipe(recipe) {
selectedRecipe.value = recipe
}
// 开始制造
function startCraftingRecipe() {
if (!selectedRecipe.value) return
const result = startCrafting(game, player, selectedRecipe.value.id)
if (result.success) {
game.addLog(result.message, 'success')
selectedRecipe.value = null
} else {
game.addLog(result.message, 'error')
}
}
// 关闭抽屉
function close() {
game.closeDrawer()
}
</script>
<style lang="scss" scoped>
.crafting-drawer {
height: 100%;
display: flex;
flex-direction: column;
background: #1a1a2e;
color: #eee;
}
.drawer-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
background: #16162a;
border-bottom: 1px solid #333;
}
.header-left {
display: flex;
flex-direction: column;
gap: 4px;
}
.drawer-title {
font-size: 18px;
font-weight: bold;
}
.drawer-subtitle {
font-size: 12px;
color: #888;
}
.drawer-close {
font-size: 28px;
color: #888;
cursor: pointer;
padding: 0 8px;
}
.crafting-skill-info {
display: flex;
align-items: center;
padding: 12px 16px;
background: #1e1e36;
gap: 8px;
}
.skill-info__label {
font-size: 12px;
color: #888;
}
.skill-info__level {
font-size: 14px;
font-weight: bold;
color: #4caf50;
}
.recipe-list {
flex: 1;
padding: 8px;
}
.recipe-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px;
margin-bottom: 8px;
background: #252540;
border-radius: 8px;
border: 1px solid #333;
transition: all 0.2s;
}
.recipe-item--disabled {
opacity: 0.5;
}
.recipe-item__main {
display: flex;
align-items: center;
gap: 12px;
flex: 1;
}
.recipe-item__icon {
font-size: 24px;
}
.recipe-item__info {
display: flex;
flex-direction: column;
gap: 4px;
flex: 1;
}
.recipe-item__name {
font-size: 14px;
font-weight: bold;
}
.recipe-item__desc {
font-size: 12px;
color: #888;
}
.recipe-materials {
display: flex;
flex-wrap: wrap;
gap: 4px;
margin-top: 4px;
}
.material-tag {
font-size: 10px;
padding: 2px 6px;
background: #1e1e36;
border-radius: 4px;
}
.material-tag--missing {
color: #f44336;
background: #3a1a1a;
}
.recipe-item__right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
}
.recipe-time {
font-size: 12px;
color: #4caf50;
}
.recipe-success-rate {
font-size: 12px;
font-weight: bold;
}
.success-rate--high {
color: #4caf50;
}
.success-rate--medium {
color: #ff9800;
}
.success-rate--low {
color: #f44336;
}
.recipe-empty {
padding: 32px;
text-align: center;
}
.recipe-empty__text {
color: #888;
}
.crafting-progress {
padding: 16px;
background: #1e1e36;
border-top: 1px solid #333;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
}
.progress-title {
font-size: 14px;
font-weight: bold;
}
.progress-percent {
font-size: 14px;
color: #4caf50;
}
.progress-bar {
height: 8px;
background: #333;
border-radius: 4px;
overflow: hidden;
}
.progress-bar__fill {
height: 100%;
background: linear-gradient(90deg, #4caf50, #8bc34a);
transition: width 0.3s;
}
.progress-item {
display: block;
margin-top: 8px;
font-size: 12px;
color: #888;
}
// 配方详情弹窗
.recipe-detail-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.8);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.recipe-detail {
width: 90%;
max-width: 400px;
max-height: 80%;
background: #1a1a2e;
border-radius: 12px;
padding: 16px;
overflow-y: auto;
}
.recipe-detail__header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.recipe-detail__icon {
font-size: 32px;
}
.recipe-detail__name {
font-size: 18px;
font-weight: bold;
}
.recipe-detail__stats {
margin-bottom: 16px;
}
.stat-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
border-bottom: 1px solid #333;
}
.stat-label {
color: #888;
}
.stat-value {
font-weight: bold;
}
.recipe-detail__materials {
margin-bottom: 16px;
}
.materials-title {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: bold;
}
.materials-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.material-item {
display: flex;
align-items: center;
gap: 8px;
padding: 8px;
background: #252540;
border-radius: 8px;
}
.material-item--missing {
background: #3a1a1a;
}
.material-item__icon {
font-size: 20px;
}
.material-item__name {
flex: 1;
}
.material-item__count {
font-weight: bold;
color: #4caf50;
}
.material-item__have {
font-size: 12px;
color: #888;
}
.recipe-detail__quality {
margin-bottom: 16px;
}
.quality-title {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: bold;
}
.quality-range {
display: flex;
align-items: center;
gap: 8px;
}
.quality-range__arrow {
color: #888;
}
.recipe-detail__actions {
display: flex;
gap: 8px;
}
</style>