Browse Source

聊天框不再折叠,该成大面板

角色信息显示在左侧,不再需要点击
master
hanqin 1 week ago
parent
commit
42fe9e904a
  1. 100
      Build_God_Game/src/components/ChatBox.vue
  2. 413
      Build_God_Game/src/views/GameView.vue

100
Build_God_Game/src/components/ChatBox.vue

@ -1,21 +1,28 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, onMounted, nextTick, computed } from 'vue' import { ref, onMounted, nextTick, computed, watch } from 'vue'
import { useChat, type ChatMessage } from '@/composables/useChat' import { useChat, type ChatMessage } from '@/composables/useChat'
import { useCharacterStore } from '@/stores/character' import { useCharacterStore } from '@/stores/character'
const props = withDefaults(
defineProps<{
/** 嵌入主布局:大面积消息区,非底部悬浮折叠条 */
embedded?: boolean
}>(),
{ embedded: false }
)
const { const {
messages, messages,
isConnected, isConnected,
isLoading, isLoading,
connect, connect,
disconnect,
sendMessage sendMessage
} = useChat() } = useChat()
const characterStore = useCharacterStore() const characterStore = useCharacterStore()
const inputText = ref('') const inputText = ref('')
const messagesContainer = ref<HTMLElement | null>(null) const messagesContainer = ref<HTMLElement | null>(null)
const isExpanded = ref(false) const isExpanded = ref(props.embedded)
const currentCharacterId = computed(() => characterStore.currentCharacter?.id) const currentCharacterId = computed(() => characterStore.currentCharacter?.id)
@ -25,6 +32,14 @@ onMounted(async () => {
scrollToBottom() scrollToBottom()
}) })
watch(
() => messages.value.length,
async () => {
await nextTick()
scrollToBottom()
}
)
const scrollToBottom = () => { const scrollToBottom = () => {
if (messagesContainer.value) { if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
@ -75,6 +90,7 @@ const formatTime = (dateStr: string) => {
} }
const toggleExpand = () => { const toggleExpand = () => {
if (props.embedded) return
isExpanded.value = !isExpanded.value isExpanded.value = !isExpanded.value
if (isExpanded.value) { if (isExpanded.value) {
nextTick(() => scrollToBottom()) nextTick(() => scrollToBottom())
@ -83,16 +99,30 @@ const toggleExpand = () => {
</script> </script>
<template> <template>
<div class="chat-box" :class="{ expanded: isExpanded }"> <div
<div class="chat-header" @click="toggleExpand"> class="chat-box"
<span class="chat-title">聊天</span> :class="{
expanded: isExpanded || embedded,
'chat-box--embedded': embedded
}"
>
<div
class="chat-header"
:class="{ 'chat-header--static': embedded }"
@click="toggleExpand"
>
<span class="chat-title">{{ embedded ? '聊天大厅' : '聊天' }}</span>
<span class="chat-status" :class="{ connected: isConnected }"> <span class="chat-status" :class="{ connected: isConnected }">
{{ isConnected ? '●' : '○' }} {{ isConnected ? '已连接' : '未连接' }}
</span> </span>
<span class="expand-icon">{{ isExpanded ? '▼' : '▲' }}</span> <span v-if="!embedded" class="expand-icon">{{ isExpanded ? '' : '' }}</span>
</div> </div>
<div v-show="isExpanded" class="chat-content"> <div
v-show="isExpanded || embedded"
class="chat-content"
:class="{ 'chat-content--embedded': embedded }"
>
<div ref="messagesContainer" class="messages-container"> <div ref="messagesContainer" class="messages-container">
<div <div
v-for="msg in messages" v-for="msg in messages"
@ -145,6 +175,22 @@ const toggleExpand = () => {
z-index: 100; z-index: 100;
} }
.chat-box--embedded {
position: relative;
max-width: none;
width: 100%;
margin: 0;
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 14px;
background: rgba(12, 14, 20, 0.92);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.45);
z-index: 5;
display: flex;
flex-direction: column;
min-height: 0;
flex: 1;
}
.chat-header { .chat-header {
display: flex; display: flex;
align-items: center; align-items: center;
@ -153,6 +199,13 @@ const toggleExpand = () => {
background: rgba(255, 255, 255, 0.03); background: rgba(255, 255, 255, 0.03);
} }
.chat-header--static {
cursor: default;
border-radius: 14px 14px 0 0;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.05);
}
.chat-title { .chat-title {
color: #aaaaaa; color: #aaaaaa;
font-size: 0.85rem; font-size: 0.85rem;
@ -160,13 +213,13 @@ const toggleExpand = () => {
} }
.chat-status { .chat-status {
font-size: 0.6rem; font-size: 0.72rem;
color: #666666; color: #666666;
margin-right: 8px; margin-right: 8px;
} }
.chat-status.connected { .chat-status.connected {
color: #44ff44; color: #4ade80;
} }
.expand-icon { .expand-icon {
@ -180,6 +233,12 @@ const toggleExpand = () => {
max-height: 250px; max-height: 250px;
} }
.chat-content--embedded {
max-height: none;
flex: 1;
min-height: 0;
}
.messages-container { .messages-container {
flex: 1; flex: 1;
overflow-y: auto; overflow-y: auto;
@ -187,7 +246,18 @@ const toggleExpand = () => {
max-height: 180px; max-height: 180px;
} }
/* 自定义滚动条 */ .chat-box--embedded .messages-container {
max-height: none;
min-height: 220px;
flex: 1;
}
@media (min-width: 900px) {
.chat-box--embedded .messages-container {
min-height: 320px;
}
}
.messages-container::-webkit-scrollbar { .messages-container::-webkit-scrollbar {
width: 6px; width: 6px;
} }
@ -207,12 +277,16 @@ const toggleExpand = () => {
} }
.message { .message {
font-size: 0.75rem; font-size: 0.8rem;
line-height: 1.6; line-height: 1.6;
padding: 2px 0; padding: 2px 0;
word-break: break-all; word-break: break-all;
} }
.chat-box--embedded .message {
font-size: 0.85rem;
}
.message-time { .message-time {
color: #555555; color: #555555;
margin-right: 6px; margin-right: 6px;

413
Build_God_Game/src/views/GameView.vue

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref } from 'vue' import { computed, ref, onMounted } from 'vue'
import { ElProgress } from 'element-plus' import { ElProgress } from 'element-plus'
import { useAuthStore } from '@/stores/auth' import { useAuthStore } from '@/stores/auth'
import { useCharacterStore } from '@/stores/character' import { useCharacterStore } from '@/stores/character'
@ -12,7 +12,6 @@ import StarBorder from '@/components/StarBorder/StarBorder.vue'
import meditationIcon from '@/assets/images/meditation.svg' import meditationIcon from '@/assets/images/meditation.svg'
import missionIcon from '@/assets/images/mission.svg' import missionIcon from '@/assets/images/mission.svg'
import scrapIcon from '@/assets/images/scrap.svg' import scrapIcon from '@/assets/images/scrap.svg'
import characterIco from '@/assets/images/character.svg'
import bagIcon from '@/assets/images/bag.svg' import bagIcon from '@/assets/images/bag.svg'
import monsterIcon from '@/assets/images/monster.svg' import monsterIcon from '@/assets/images/monster.svg'
import shopIcon from '@/assets/images/shop.svg' import shopIcon from '@/assets/images/shop.svg'
@ -44,7 +43,6 @@ const showBreakthroughMessage = ref(false)
const menuItems = computed(() => [ const menuItems = computed(() => [
{ label: '任务', icon: missionIcon, useImage: true }, { label: '任务', icon: missionIcon, useImage: true },
{ label: '角色', icon: characterIco, useImage: true },
{ label: isTraining.value ? '打坐中' : '打坐', icon: meditationIcon, useImage: true, isTraining: isTraining.value }, { label: isTraining.value ? '打坐中' : '打坐', icon: meditationIcon, useImage: true, isTraining: isTraining.value },
{ label: '背包', icon: bagIcon, useImage: true }, { label: '背包', icon: bagIcon, useImage: true },
{ label: '捡垃圾', icon: scrapIcon, useImage: true }, { label: '捡垃圾', icon: scrapIcon, useImage: true },
@ -84,23 +82,11 @@ const navigateTo = (item: { label: string }) => {
router.push('/monster-list') router.push('/monster-list')
} else if (item.label === '商店') { } else if (item.label === '商店') {
router.push('/shop') router.push('/shop')
} else if (item.label === '角色') {
openCharacterDetail()
} else if (item.label === '图鉴') { } else if (item.label === '图鉴') {
router.push('/catalog') router.push('/catalog')
} }
} }
const openCharacterDetail = async () => {
await characterStore.fetchCharacters()
const current = characterStore.characters.find(c => c.id === characterStore.currentCharacter?.id)
if (current) {
characterStore.currentCharacter = current
localStorage.setItem('current_character', JSON.stringify(current))
}
showCharacterDetail.value = true
}
const handleBreakthrough = async () => { const handleBreakthrough = async () => {
const result = await characterStore.breakthrough() const result = await characterStore.breakthrough()
breakthroughMessage.value = result.message breakthroughMessage.value = result.message
@ -110,29 +96,96 @@ const handleBreakthrough = async () => {
}, 3000) }, 3000)
} }
const showCharacterDetail = ref(false) onMounted(async () => {
await characterStore.fetchCharacters()
const closeCharacterDetail = () => { const current = characterStore.characters.find(c => c.id === characterStore.currentCharacter?.id)
showCharacterDetail.value = false if (current) {
} characterStore.currentCharacter = current
localStorage.setItem('current_character', JSON.stringify(current))
}
})
</script> </script>
<template> <template>
<div class="game-page"> <div class="game-page">
<Particles :particle-count="50" :particle-colors="['#ffffff', '#cccccc']" class="particles-bg" /> <Particles :particle-count="50" :particle-colors="['#ffffff', '#cccccc']" class="particles-bg" />
<div class="game-container"> <div class="game-shell">
<div style="text-align: center; margin-bottom: 20px;"> <aside id="character-sidebar" class="character-sidebar">
<StarBorder as="div" color="#22c55e" speed="5s" :thickness="2" <div class="sidebar-card">
style="display: block; width: 100%; max-width: 480px;"> <div class="sidebar-header">
<div class="character-header" @click="handleSwitchCharacter"> <div class="sidebar-title-block">
<div class="character-info"> <span class="sidebar-char-name">{{ characterStore.currentCharacter?.name || '未选择角色' }}</span>
<span class="character-name">{{ characterStore.currentCharacter?.name || '未选择角色' }}</span> <span class="sidebar-char-level">{{ characterStore.currentCharacter?.levelName || '' }}</span>
<span class="character-level">{{ characterStore.currentCharacter?.levelName || '' }}</span> </div>
<button type="button" class="sidebar-switch-btn" @click="handleSwitchCharacter">
切换角色
</button>
</div> </div>
<span class="switch-btn">切换角色</span>
<div class="detail-section">
<div class="section-title">基础信息</div>
<div class="info-row">
<span class="info-label">职业</span>
<span class="info-value">{{ characterStore.currentCharacter?.professionName || '未选择' }}</span>
</div> </div>
</StarBorder> <div class="info-row">
<span class="info-label">灵石</span>
<span class="info-value">{{ formatNumber(characterStore.currentCharacter?.money || 0) }}</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">属性</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon hp-icon"></span>生命</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseMaxHP || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusMaxHP || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusMaxHP || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon atk-icon"></span>攻击</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseAttack || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusAttack || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusAttack || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon def-icon">🛡</span>防御</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseDefend || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusDefend || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusDefend || 0) }}</span>
</span>
</div> </div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon crit-icon">💥</span>暴击率</span>
<span class="attr-value">
<span class="base-value">{{ (characterStore.currentCharacter?.baseCriticalRate || 0).toFixed(1) }}%</span>
<span v-if="(characterStore.currentCharacter?.bonusCriticalRate || 0) > 0" class="bonus-value">+{{
(characterStore.currentCharacter?.bonusCriticalRate || 0).toFixed(1) }}%</span>
</span>
</div>
</div>
<div class="detail-section detail-section--last">
<div class="section-title">经验</div>
<div class="exp-row">
<span class="exp-label">当前经验</span>
<span class="exp-value">{{ formatNumber(characterStore.currentCharacter?.currentExp || 0) }}</span>
</div>
<div v-if="characterStore.currentCharacter?.nextLevelMinExp" class="exp-row">
<span class="exp-label">升级所需</span>
<span class="exp-value">{{ formatNumber(characterStore.currentCharacter?.nextLevelMinExp || 0) }}</span>
</div>
</div>
</div>
</aside>
<div class="game-main">
<div class="game-container">
<div style="text-align: center;"> <div style="text-align: center;">
<ShinyText class="welcome-text" text="✨ 欢迎来到我的世界" :speed="2" :delay="0.5" :disabled="false" :color="'#b5b5b5'" <ShinyText class="welcome-text" text="✨ 欢迎来到我的世界" :speed="2" :delay="0.5" :disabled="false" :color="'#b5b5b5'"
:shine-color="'#34fef1'" :spread="120" :direction="'left'" :yoyo="false" :pause-on-hover="false" /> :shine-color="'#34fef1'" :spread="120" :direction="'left'" :yoyo="false" :pause-on-hover="false" />
@ -197,87 +250,10 @@ const closeCharacterDetail = () => {
</div> </div>
</StarBorder> </StarBorder>
</div> </div>
<ChatBox />
</div>
<div v-if="showCharacterDetail" class="character-detail-overlay" @click="closeCharacterDetail">
<div class="character-detail-dialog" @click.stop>
<div class="detail-header">
<span class="detail-title">角色详情</span>
<span class="detail-close" @click="closeCharacterDetail">×</span>
</div>
<div class="detail-section">
<div class="section-title">基础信息</div>
<div class="info-row">
<span class="info-label">角色名</span>
<span class="info-value">{{ characterStore.currentCharacter?.name }}</span>
</div>
<div class="info-row">
<span class="info-label">职业</span>
<span class="info-value">{{ characterStore.currentCharacter?.professionName || '未选择' }}</span>
</div>
<div class="info-row">
<span class="info-label">境界</span>
<span class="info-value">{{ characterStore.currentCharacter?.levelName }}</span>
</div>
<div class="info-row">
<span class="info-label">灵石</span>
<span class="info-value">{{ formatNumber(characterStore.currentCharacter?.money || 0) }}</span>
</div>
</div>
<div class="detail-section">
<div class="section-title">属性</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon hp-icon"></span>生命</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseMaxHP || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusMaxHP || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusMaxHP || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon atk-icon"></span>攻击</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseAttack || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusAttack || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusAttack || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon def-icon">🛡</span>防御</span>
<span class="attr-value">
<span class="base-value">{{ formatNumber(characterStore.currentCharacter?.baseDefend || 0) }}</span>
<span v-if="(characterStore.currentCharacter?.bonusDefend || 0) > 0" class="bonus-value">+{{
formatNumber(characterStore.currentCharacter?.bonusDefend || 0) }}</span>
</span>
</div>
<div class="attr-row">
<span class="attr-label"><span class="attr-icon crit-icon">💥</span>暴击率</span>
<span class="attr-value">
<span class="base-value">{{ (characterStore.currentCharacter?.baseCriticalRate || 0).toFixed(1) }}%</span>
<span v-if="(characterStore.currentCharacter?.bonusCriticalRate || 0) > 0" class="bonus-value">+{{
(characterStore.currentCharacter?.bonusCriticalRate || 0).toFixed(1) }}%</span>
</span>
</div>
</div> </div>
<div class="detail-section"> <div class="chat-panel">
<div class="section-title">经验</div> <ChatBox embedded />
<div class="exp-row">
<span class="exp-label">当前经验</span>
<span class="exp-value">{{ formatNumber(characterStore.currentCharacter?.currentExp || 0) }}</span>
</div>
<div v-if="characterStore.currentCharacter?.nextLevelMinExp" class="exp-row">
<span class="exp-label">升级所需</span>
<span class="exp-value">{{ formatNumber(characterStore.currentCharacter?.nextLevelMinExp || 0) }}</span>
</div>
</div>
<div class="detail-footer">
<button class="switch-character-btn" @click="handleSwitchCharacter">切换角色</button>
</div> </div>
</div> </div>
</div> </div>
@ -288,10 +264,10 @@ const closeCharacterDetail = () => {
.game-page { .game-page {
min-height: 100vh; min-height: 100vh;
background: #000000; background: #000000;
padding: 20px; padding: 16px;
padding-bottom: 60px; padding-bottom: 24px;
position: relative; position: relative;
overflow: hidden; overflow-x: hidden;
} }
.particles-bg { .particles-bg {
@ -302,60 +278,141 @@ const closeCharacterDetail = () => {
height: 100% !important; height: 100% !important;
} }
.game-container { .game-shell {
max-width: 480px;
margin: 0 auto;
padding-top: 20px;
position: relative; position: relative;
z-index: 10; z-index: 10;
display: flex;
flex-direction: row;
align-items: stretch;
gap: 20px;
max-width: 1200px;
margin: 0 auto;
min-height: calc(100vh - 48px);
} }
.character-header { .character-sidebar {
display: flex; flex: 0 0 280px;
justify-content: space-between; min-width: 0;
align-items: center; align-self: flex-start;
background: rgba(255, 255, 255, 0.03); position: sticky;
border: 1px solid rgba(255, 255, 255, 0.08); top: 16px;
border-radius: 12px; max-height: calc(100vh - 32px);
padding: 12px 16px; overflow-y: auto;
cursor: pointer;
transition: all 0.2s ease;
height: 100%;
} }
.character-header:hover { .sidebar-card {
background: rgba(255, 255, 255, 0.05); background: rgba(16, 18, 24, 0.92);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
padding: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.35);
} }
.character-info { .sidebar-header {
display: flex; display: flex;
align-items: center; flex-direction: column;
gap: 12px; gap: 12px;
margin-bottom: 16px;
padding-bottom: 14px;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.sidebar-title-block {
display: flex;
flex-direction: column;
gap: 4px;
} }
.character-name { .sidebar-char-name {
color: #ffffff; color: #ffffff;
font-size: 1rem; font-size: 1.1rem;
font-weight: 500; font-weight: 600;
} }
.character-level { .sidebar-char-level {
color: #888888; color: #22c55e;
font-size: 0.8rem; font-size: 0.8rem;
} }
.switch-btn { .sidebar-switch-btn {
color: #666666; width: 100%;
font-size: 0.75rem; padding: 10px 12px;
background: rgba(34, 197, 94, 0.12);
border: 1px solid rgba(34, 197, 94, 0.35);
border-radius: 10px;
color: #86efac;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.2s ease;
}
.sidebar-switch-btn:hover {
background: rgba(34, 197, 94, 0.2);
color: #bbf7d0;
}
.detail-section--last {
margin-bottom: 0;
}
.game-main {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 16px;
min-height: 0;
}
.game-container {
max-width: 560px;
width: 100%;
margin: 0 auto;
padding-top: 8px;
position: relative;
z-index: 10;
flex-shrink: 0;
}
.chat-panel {
flex: 1;
min-height: 280px;
display: flex;
flex-direction: column;
min-width: 0;
}
@media (max-width: 900px) {
.game-shell {
flex-direction: column;
min-height: auto;
}
.character-sidebar {
flex: none;
width: 100%;
max-width: 560px;
margin: 0 auto;
position: relative;
top: 0;
max-height: none;
}
.chat-panel {
min-height: 240px;
max-width: 560px;
margin: 0 auto;
width: 100%;
}
} }
.welcome-text { .welcome-text {
font-size: 1.5rem; font-size: 1.35rem;
font-weight: 300; font-weight: 300;
text-align: center; text-align: center;
color: #ffffff; color: #ffffff;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: 24px; margin-bottom: 20px;
font-weight: bold; font-weight: bold;
} }
@ -620,58 +677,8 @@ const closeCharacterDetail = () => {
color: #ffffff; color: #ffffff;
} }
.character-detail-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.character-detail-dialog {
background: rgba(20, 20, 25, 0.95);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
width: 90%;
max-width: 400px;
max-height: 80vh;
overflow-y: auto;
padding: 20px;
}
.detail-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.detail-title {
color: #ffffff;
font-size: 1.2rem;
font-weight: 600;
}
.detail-close {
color: #666666;
font-size: 1.5rem;
cursor: pointer;
line-height: 1;
}
.detail-close:hover {
color: #ffffff;
}
.detail-section { .detail-section {
margin-bottom: 20px; margin-bottom: 16px;
} }
.section-title { .section-title {
@ -768,26 +775,4 @@ const closeCharacterDetail = () => {
color: #cccccc; color: #cccccc;
font-size: 0.85rem; font-size: 0.85rem;
} }
.detail-footer {
margin-top: 20px;
text-align: center;
}
.switch-character-btn {
padding: 12px 24px;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 10px;
color: #888888;
font-size: 0.9rem;
cursor: pointer;
transition: all 0.2s ease;
}
.switch-character-btn:hover {
background: rgba(255, 255, 255, 0.1);
border-color: rgba(255, 255, 255, 0.25);
color: #ffffff;
}
</style> </style>

Loading…
Cancel
Save