feat: add active domains serialization and UI integration
- Implemented `serialize_active_domains` function to gather and format active hidden domains from the game world. - Updated `game_loop` to include active domains in the broadcast state. - Enhanced `StatusBar` component to display active domains with a new `StatusWidget`, including dynamic labels and colors based on the number of active domains. - Added localization support for hidden domain messages in both English and Chinese. - Updated the world store to manage active domains state and synchronize with the backend.
This commit is contained in:
@@ -4,6 +4,7 @@ import { useSocketStore } from '../../stores/socket'
|
||||
import { ref, computed } from 'vue'
|
||||
import { NPopover, NModal, NList, NListItem, NTag, NEmpty, useMessage } from 'naive-ui'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import StatusWidget from './StatusWidget.vue'
|
||||
|
||||
const { t } = useI18n()
|
||||
const store = useWorldStore()
|
||||
@@ -17,6 +18,17 @@ const phenomenonColor = computed(() => {
|
||||
return getRarityColor(p.rarity);
|
||||
})
|
||||
|
||||
const domainLabel = computed(() => {
|
||||
const count = store.activeDomains.length;
|
||||
return count > 0
|
||||
? t('game.status_bar.hidden_domain.label_active', { count })
|
||||
: t('game.status_bar.hidden_domain.label');
|
||||
});
|
||||
|
||||
const domainColor = computed(() => {
|
||||
return store.activeDomains.length > 0 ? '#fa8c16' : '#666'; // 有秘境时亮橙色,否则灰色
|
||||
});
|
||||
|
||||
function getRarityColor(rarity: string) {
|
||||
switch (rarity) {
|
||||
case 'N': return '#ccc';
|
||||
@@ -26,17 +38,8 @@ function getRarityColor(rarity: string) {
|
||||
default: return '#ccc';
|
||||
}
|
||||
}
|
||||
|
||||
async function openPhenomenonSelector() {
|
||||
showSelector.value = true;
|
||||
await store.getPhenomenaList();
|
||||
}
|
||||
|
||||
async function handleSelect(id: number, name: string) {
|
||||
await store.changePhenomenon(id);
|
||||
showSelector.value = false;
|
||||
message.success(t('game.status_bar.change_success', { name }));
|
||||
}
|
||||
// ...
|
||||
// ...
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -49,18 +52,14 @@ async function handleSelect(id: number, name: string) {
|
||||
<span class="time">{{ store.year }}{{ t('common.year') }} {{ store.month }}{{ t('common.month') }}</span>
|
||||
|
||||
<!-- 天地灵机 -->
|
||||
<div class="phenomenon" v-if="store.currentPhenomenon">
|
||||
<span class="divider">|</span>
|
||||
<n-popover trigger="hover" placement="bottom" style="max-width: 300px;">
|
||||
<template #trigger>
|
||||
<span
|
||||
class="phenomenon-name"
|
||||
:style="{ color: phenomenonColor }"
|
||||
@click="openPhenomenonSelector"
|
||||
>
|
||||
[{{ store.currentPhenomenon.name }}]
|
||||
</span>
|
||||
</template>
|
||||
<StatusWidget
|
||||
v-if="store.currentPhenomenon"
|
||||
:label="`[${store.currentPhenomenon.name}]`"
|
||||
:color="phenomenonColor"
|
||||
mode="single"
|
||||
@trigger-click="openPhenomenonSelector"
|
||||
>
|
||||
<template #single>
|
||||
<div class="phenomenon-card">
|
||||
<div class="p-header" :style="{ color: phenomenonColor }">
|
||||
<span class="p-title">{{ store.currentPhenomenon.name }}</span>
|
||||
@@ -79,8 +78,18 @@ async function handleSelect(id: number, name: string) {
|
||||
</div>
|
||||
<div class="click-tip">{{ t('game.status_bar.click_to_change') }}</div>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
</template>
|
||||
</StatusWidget>
|
||||
|
||||
<!-- 秘境 -->
|
||||
<StatusWidget
|
||||
:label="domainLabel"
|
||||
:color="domainColor"
|
||||
mode="list"
|
||||
:title="t('game.status_bar.hidden_domain.title')"
|
||||
:items="store.activeDomains"
|
||||
:empty-text="t('game.status_bar.hidden_domain.empty')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 天象选择器 Modal -->
|
||||
@@ -158,26 +167,7 @@ async function handleSelect(id: number, name: string) {
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.phenomenon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
color: #444;
|
||||
}
|
||||
|
||||
.phenomenon-name {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.phenomenon-name:hover {
|
||||
opacity: 0.8;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* .phenomenon, .divider, .phenomenon-name REMOVED (moved to StatusWidget) */
|
||||
|
||||
.phenomenon-card {
|
||||
padding: 4px 0;
|
||||
|
||||
106
web/src/components/layout/StatusWidget.vue
Normal file
106
web/src/components/layout/StatusWidget.vue
Normal file
@@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { NPopover, NList, NListItem, NTag, NEmpty } from 'naive-ui'
|
||||
import type { HiddenDomainInfo } from '../../types/core'
|
||||
|
||||
interface Props {
|
||||
// 触发器显示
|
||||
label: string
|
||||
color?: string
|
||||
|
||||
// 弹窗内容
|
||||
title?: string
|
||||
items?: HiddenDomainInfo[] // 通用列表数据 (这里暂时专用于秘境,如果未来需要其他类型再泛型化)
|
||||
emptyText?: string
|
||||
|
||||
// 模式: 'single' (天地灵机) 或 'list' (秘境)
|
||||
mode?: 'single' | 'list'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
color: '#ccc',
|
||||
items: () => [],
|
||||
mode: 'list',
|
||||
emptyText: '暂无数据'
|
||||
})
|
||||
|
||||
// 发射点击事件(用于天地灵机的"更易天象")
|
||||
const emit = defineEmits(['trigger-click'])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="status-widget">
|
||||
<span class="divider">|</span>
|
||||
<n-popover trigger="click" placement="bottom" style="max-width: 350px;">
|
||||
<template #trigger>
|
||||
<span
|
||||
class="widget-trigger"
|
||||
:style="{ color: props.color }"
|
||||
@click="emit('trigger-click')"
|
||||
>
|
||||
{{ props.label }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- 弹窗内容区 -->
|
||||
<div class="widget-content">
|
||||
<!-- 模式A: 单个详情 (复用天地灵机样式) -->
|
||||
<slot name="single" v-if="mode === 'single'"></slot>
|
||||
|
||||
<!-- 模式B: 列表展示 (用于秘境) -->
|
||||
<div v-else-if="mode === 'list'" class="list-container">
|
||||
<div class="list-header" v-if="title">{{ title }}</div>
|
||||
|
||||
<n-list v-if="items.length > 0" hoverable clickable>
|
||||
<n-list-item v-for="item in items" :key="item.id">
|
||||
<div class="domain-item">
|
||||
<div class="d-header">
|
||||
<span class="d-name">{{ item.name }}</span>
|
||||
<n-tag size="small" :bordered="false" type="warning" class="d-tag">
|
||||
{{ item.max_realm }}
|
||||
</n-tag>
|
||||
</div>
|
||||
<div class="d-desc">{{ item.desc }}</div>
|
||||
<div class="d-stats">
|
||||
<span>💀 {{ (item.danger_prob * 100).toFixed(0) }}%</span>
|
||||
<span>🎁 {{ (item.drop_prob * 100).toFixed(0) }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<n-empty v-else :description="emptyText" class="empty-state" />
|
||||
</div>
|
||||
</div>
|
||||
</n-popover>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.widget-trigger {
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
.widget-trigger:hover { opacity: 0.8; }
|
||||
.divider { color: #444; margin-right: 10px; }
|
||||
|
||||
.list-header {
|
||||
font-weight: bold;
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid #333;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.domain-item { padding: 4px 0; }
|
||||
.d-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
|
||||
.d-name { font-weight: bold; color: #fadb14; font-size: 14px; }
|
||||
.d-tag { font-size: 10px; height: 18px; line-height: 18px; }
|
||||
.d-desc { font-size: 12px; color: #aaa; margin-bottom: 8px; line-height: 1.4; }
|
||||
.d-stats { display: flex; gap: 12px; font-size: 12px; color: #888; }
|
||||
.empty-state { padding: 20px; }
|
||||
|
||||
/* Naive UI List Override */
|
||||
:deep(.n-list-item) {
|
||||
padding: 8px 12px !important;
|
||||
}
|
||||
</style>
|
||||
@@ -230,7 +230,15 @@
|
||||
"change_success": "Phenomenon changed to: {name}",
|
||||
"click_to_change": "(Click to change phenomenon)",
|
||||
"author_bilibili": "Bilibili",
|
||||
"author_github": "Github"
|
||||
"author_github": "Github",
|
||||
"hidden_domain": {
|
||||
"label": "[Hidden Domain]",
|
||||
"label_active": "[Domain Opened: {count}]",
|
||||
"title": "Opened Hidden Domains",
|
||||
"empty": "No hidden domains currently open",
|
||||
"danger": "Danger",
|
||||
"drop": "Fortune"
|
||||
}
|
||||
},
|
||||
"controls": {
|
||||
"resume": "Resume Game",
|
||||
|
||||
@@ -230,7 +230,15 @@
|
||||
"change_success": "天象已更易为:{name}",
|
||||
"click_to_change": "(点击可更易天象)",
|
||||
"author_bilibili": "B站空间",
|
||||
"author_github": "Github仓库"
|
||||
"author_github": "Github仓库",
|
||||
"hidden_domain": {
|
||||
"label": "[秘境]",
|
||||
"label_active": "[秘境开启: {count}]",
|
||||
"title": "当前开启秘境",
|
||||
"empty": "当前暂无秘境开启",
|
||||
"danger": "凶险",
|
||||
"drop": "机缘"
|
||||
}
|
||||
},
|
||||
"controls": {
|
||||
"resume": "继续游戏",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref, shallowRef, computed } from 'vue';
|
||||
import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary, CelestialPhenomenon } from '../types/core';
|
||||
import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary, CelestialPhenomenon, HiddenDomainInfo } from '../types/core';
|
||||
import type { TickPayloadDTO, InitialStateDTO } from '../types/api';
|
||||
import type { FetchEventsParams } from '../types/api';
|
||||
import { worldApi, eventApi } from '../api';
|
||||
@@ -33,6 +33,9 @@ export const useWorldStore = defineStore('world', () => {
|
||||
const currentPhenomenon = ref<CelestialPhenomenon | null>(null);
|
||||
const phenomenaList = shallowRef<CelestialPhenomenon[]>([]);
|
||||
|
||||
// 秘境列表
|
||||
const activeDomains = shallowRef<HiddenDomainInfo[]>([]);
|
||||
|
||||
// 请求计数器,用于处理 loadEvents 的竞态条件。
|
||||
let eventsRequestId = 0;
|
||||
// 请求计数器,用于处理 fetchState 的竞态条件。
|
||||
@@ -110,6 +113,13 @@ export const useWorldStore = defineStore('world', () => {
|
||||
if (payload.phenomenon !== undefined) {
|
||||
currentPhenomenon.value = payload.phenomenon;
|
||||
}
|
||||
// 处理秘境同步
|
||||
if (payload.active_domains !== undefined) {
|
||||
activeDomains.value = payload.active_domains;
|
||||
} else {
|
||||
// 如果后端不传,说明本回合无秘境,清空
|
||||
activeDomains.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
function applyStateSnapshot(stateRes: InitialStateDTO) {
|
||||
@@ -126,6 +136,7 @@ export const useWorldStore = defineStore('world', () => {
|
||||
eventsFilter.value = {};
|
||||
currentPhenomenon.value = stateRes.phenomenon || null;
|
||||
isLoaded.value = true;
|
||||
activeDomains.value = [];
|
||||
}
|
||||
|
||||
// 提前加载地图数据(在 LLM 初始化期间可用)。
|
||||
@@ -226,6 +237,7 @@ export const useWorldStore = defineStore('world', () => {
|
||||
eventsFilter.value = {};
|
||||
isLoaded.value = false;
|
||||
currentPhenomenon.value = null;
|
||||
activeDomains.value = [];
|
||||
}
|
||||
|
||||
// --- 事件分页 ---
|
||||
@@ -358,6 +370,7 @@ export const useWorldStore = defineStore('world', () => {
|
||||
loadMoreEvents,
|
||||
resetEvents,
|
||||
getPhenomenaList,
|
||||
changePhenomenon
|
||||
changePhenomenon,
|
||||
activeDomains
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* 这些类型严格对应后端接口返回的 JSON 结构。
|
||||
*/
|
||||
|
||||
import type { MapMatrix, CelestialPhenomenon } from './core';
|
||||
import type { MapMatrix, CelestialPhenomenon, HiddenDomainInfo } from './core';
|
||||
|
||||
// --- 通用响应 ---
|
||||
|
||||
@@ -39,6 +39,7 @@ export interface TickPayloadDTO {
|
||||
avatars?: Array<Partial<InitialStateDTO['avatars'] extends (infer U)[] ? U : never>>;
|
||||
events?: unknown[];
|
||||
phenomenon?: CelestialPhenomenon | null;
|
||||
active_domains?: HiddenDomainInfo[];
|
||||
}
|
||||
|
||||
export interface MapResponseDTO {
|
||||
|
||||
@@ -183,6 +183,18 @@ export interface CelestialPhenomenon {
|
||||
effect_desc?: string;
|
||||
}
|
||||
|
||||
// web/src/types/core.ts
|
||||
|
||||
// 新增秘境信息接口
|
||||
export interface HiddenDomainInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
max_realm: string; // 限制境界
|
||||
danger_prob: number; // 凶险度 (0.0 - 1.0)
|
||||
drop_prob: number; // 机缘度 (0.0 - 1.0)
|
||||
}
|
||||
|
||||
// --- 事件 (Events) ---
|
||||
|
||||
export interface GameEvent {
|
||||
|
||||
Reference in New Issue
Block a user