refactor frontend
This commit is contained in:
@@ -3,7 +3,7 @@ import { onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { NConfigProvider, darkTheme, NMessageProvider } from 'naive-ui'
|
||||
import { useWorldStore } from './stores/world'
|
||||
import { useUiStore } from './stores/ui'
|
||||
import { useGameSocket } from './composables/useGameSocket'
|
||||
import { useSocketStore } from './stores/socket'
|
||||
import { gameApi } from './api/game'
|
||||
|
||||
import GameCanvas from './components/game/GameCanvas.vue'
|
||||
@@ -12,20 +12,24 @@ import StatusBar from './components/layout/StatusBar.vue'
|
||||
import EventPanel from './components/panels/EventPanel.vue'
|
||||
import SystemMenu from './components/SystemMenu.vue'
|
||||
|
||||
// Composables
|
||||
useGameSocket()
|
||||
|
||||
// Stores
|
||||
const worldStore = useWorldStore()
|
||||
const uiStore = useUiStore()
|
||||
const socketStore = useSocketStore()
|
||||
|
||||
const showMenu = ref(false)
|
||||
const isManualPaused = ref(false)
|
||||
|
||||
onMounted(async () => {
|
||||
// 初始化 Socket 连接
|
||||
socketStore.init()
|
||||
// 初始化世界状态
|
||||
await worldStore.initialize()
|
||||
window.addEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
socketStore.disconnect()
|
||||
window.removeEventListener('keydown', handleKeydown)
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
* 封装基础的 fetch 请求
|
||||
*/
|
||||
|
||||
const API_BASE = ''; // 相对路径
|
||||
// 使用环境变量作为 API 基础路径,如果没有配置则默认为空(相对路径)
|
||||
const API_BASE = import.meta.env.VITE_API_TARGET || '';
|
||||
|
||||
export class ApiError extends Error {
|
||||
public status: number;
|
||||
|
||||
@@ -3,13 +3,13 @@ import { onMounted, ref, watch } from 'vue'
|
||||
import { Container, Sprite } from 'pixi.js'
|
||||
import { useTextures } from './composables/useTextures'
|
||||
import { useWorldStore } from '../../stores/world'
|
||||
import { getRegionTextStyle } from '../../utils/mapStyles'
|
||||
import type { RegionSummary } from '../../types/core'
|
||||
|
||||
const TILE_SIZE = 64
|
||||
const mapContainer = ref<Container>()
|
||||
const { textures, isLoaded, loadSectTexture, loadCityTexture } = useTextures()
|
||||
const worldStore = useWorldStore()
|
||||
const regionStyleCache = new Map<string, Record<string, unknown>>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'mapLoaded', payload: { width: number, height: number }): void
|
||||
@@ -49,7 +49,8 @@ async function renderMap() {
|
||||
let tex = textures.value[type]
|
||||
|
||||
if (type === 'SECT') {
|
||||
tex = resolveSectTexture(x, y) ?? textures.value['CITY']
|
||||
// Legacy placeholder
|
||||
tex = textures.value['CITY']
|
||||
}
|
||||
|
||||
if (!tex) {
|
||||
@@ -96,9 +97,9 @@ async function preloadRegionTextures() {
|
||||
// Cities
|
||||
const cityNames = Array.from(
|
||||
new Set(
|
||||
regions
|
||||
.filter(region => region.type === 'city')
|
||||
.map(region => region.name)
|
||||
regions
|
||||
.filter(region => region.type === 'city')
|
||||
.map(region => region.name)
|
||||
)
|
||||
)
|
||||
|
||||
@@ -108,12 +109,6 @@ async function preloadRegionTextures() {
|
||||
])
|
||||
}
|
||||
|
||||
// Sect tile rendering is now handled in renderLargeRegions via slices
|
||||
function resolveSectTexture(_x: number, _y: number) {
|
||||
// Legacy function - sect rendering is now done via slices in renderLargeRegions
|
||||
return null
|
||||
}
|
||||
|
||||
function renderLargeRegions() {
|
||||
const regions = Array.from(worldStore.regions.values());
|
||||
for (const region of regions) {
|
||||
@@ -159,28 +154,6 @@ function renderLargeRegions() {
|
||||
}
|
||||
}
|
||||
|
||||
function getRegionStyle(type: string) {
|
||||
if (regionStyleCache.has(type)) {
|
||||
return regionStyleCache.get(type)
|
||||
}
|
||||
const style = {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: type === 'sect' ? 60 : 72,
|
||||
fill: type === 'sect' ? '#ffcc00' : (type === 'city' ? '#ccffcc' : '#ffffff'),
|
||||
stroke: { color: '#000000', width: 5, join: 'round' },
|
||||
align: 'center',
|
||||
dropShadow: {
|
||||
color: '#000000',
|
||||
blur: 3,
|
||||
angle: Math.PI / 6,
|
||||
distance: 3,
|
||||
alpha: 0.8
|
||||
}
|
||||
}
|
||||
regionStyleCache.set(type, style)
|
||||
return style
|
||||
}
|
||||
|
||||
function handleRegionSelect(region: RegionSummary) {
|
||||
emit('regionSelected', {
|
||||
type: 'region',
|
||||
@@ -205,7 +178,7 @@ function handleRegionSelect(region: RegionSummary) {
|
||||
:x="r.x * TILE_SIZE + TILE_SIZE / 2"
|
||||
:y="r.y * TILE_SIZE + TILE_SIZE * 1.5"
|
||||
:anchor="0.5"
|
||||
:style="getRegionStyle(r.type)"
|
||||
:style="getRegionTextStyle(r.type)"
|
||||
event-mode="static"
|
||||
cursor="pointer"
|
||||
@pointertap="handleRegionSelect(r)"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { onClickOutside } from '@vueuse/core';
|
||||
import { useUiStore } from '../../../../stores/ui';
|
||||
|
||||
// Sub-components
|
||||
@@ -11,15 +12,19 @@ const uiStore = useUiStore();
|
||||
const panelRef = ref<HTMLElement | null>(null);
|
||||
let lastOpenAt = 0;
|
||||
|
||||
// --- Computed ---
|
||||
// --- Component Resolution ---
|
||||
|
||||
const currentComponent = computed(() => {
|
||||
if (uiStore.selectedTarget?.type === 'avatar') return AvatarDetailView;
|
||||
if (uiStore.selectedTarget?.type === 'region') return RegionDetailView;
|
||||
if (uiStore.selectedTarget?.type === 'sect') return SectDetailView;
|
||||
return null;
|
||||
switch (uiStore.selectedTarget?.type) {
|
||||
case 'avatar': return AvatarDetailView;
|
||||
case 'region': return RegionDetailView;
|
||||
case 'sect': return SectDetailView;
|
||||
default: return null;
|
||||
}
|
||||
});
|
||||
|
||||
// --- Title & Subtitle Logic ---
|
||||
|
||||
const title = computed(() => {
|
||||
if (uiStore.detailData) {
|
||||
return uiStore.detailData.name;
|
||||
@@ -53,35 +58,20 @@ function close() {
|
||||
showNicknameReason.value = false;
|
||||
}
|
||||
|
||||
// Click outside to close
|
||||
function handleDocumentPointerDown(event: PointerEvent) {
|
||||
if (!uiStore.selectedTarget || !panelRef.value) return;
|
||||
const target = event.target as Node | null;
|
||||
|
||||
// 检查点击是否在面板内部,或者是在二级弹窗内部(如果二级弹窗是挂在 body 上的 portal 这里的逻辑会有问题,但目前 SecondaryPopup 是 AvatarDetail 的子组件,所以应该被 panelRef 包含)
|
||||
// 但是要注意,SecondaryPopup 使用了 absolute 定位到 left: -100%,仍然是 panelRef 的子节点
|
||||
if (target && panelRef.value.contains(target)) return;
|
||||
// 使用 vueuse 的 onClickOutside 替代原生监听
|
||||
onClickOutside(panelRef, () => {
|
||||
// Prevent closing immediately after opening if click propagated
|
||||
const now = performance.now();
|
||||
if (now - lastOpenAt < 100) return;
|
||||
|
||||
close();
|
||||
});
|
||||
|
||||
// Prevent closing immediately after opening if click propagated
|
||||
const now = performance.now();
|
||||
if (now - lastOpenAt < 100) return;
|
||||
|
||||
close();
|
||||
}
|
||||
|
||||
// Record open time
|
||||
// Record open time to prevent immediate close
|
||||
watch(() => uiStore.selectedTarget, (val) => {
|
||||
if (val) lastOpenAt = performance.now();
|
||||
showNicknameReason.value = false;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('pointerdown', handleDocumentPointerDown);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('pointerdown', handleDocumentPointerDown);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -126,17 +116,6 @@ onUnmounted(() => {
|
||||
:data="uiStore.detailData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Fallback / Legacy Hover Info (If detail failed or not applicable) -->
|
||||
<div v-else class="legacy-list">
|
||||
<div v-for="(line, i) in uiStore.hoverInfo" :key="i" class="line">
|
||||
<span
|
||||
v-for="(seg, j) in line"
|
||||
:key="j"
|
||||
:style="{ color: seg.color }"
|
||||
>{{ seg.text }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -148,10 +127,10 @@ onUnmounted(() => {
|
||||
right: 20px;
|
||||
width: 320px;
|
||||
max-height: calc(100vh - 80px);
|
||||
background: rgba(24, 24, 24, 0.96);
|
||||
border: 1px solid #333;
|
||||
background: var(--panel-bg);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.45);
|
||||
box-shadow: var(--panel-shadow);
|
||||
color: #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -164,8 +143,8 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 16px;
|
||||
background: rgba(38, 38, 38, 0.95);
|
||||
border-bottom: 1px solid #333;
|
||||
background: var(--panel-header-bg);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
flex-shrink: 0;
|
||||
@@ -180,7 +159,7 @@ onUnmounted(() => {
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.sub-title.clickable {
|
||||
@@ -206,14 +185,13 @@ onUnmounted(() => {
|
||||
width: max-content;
|
||||
max-width: 260px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
||||
pointer-events: none; /* Prevents blocking if it overlaps something vital, though typically we want to select text */
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.popover-arrow {
|
||||
position: absolute;
|
||||
top: -5px;
|
||||
left: 20px; /* Approximate alignment under the nickname */
|
||||
left: 20px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: rgba(50, 50, 50, 0.98);
|
||||
@@ -233,11 +211,6 @@ onUnmounted(() => {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.sub-title {
|
||||
font-size: 12px;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -254,7 +227,7 @@ onUnmounted(() => {
|
||||
|
||||
.panel-body {
|
||||
flex: 1;
|
||||
overflow: hidden; /* Let children scroll */
|
||||
overflow: hidden;
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -270,7 +243,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.state-msg {
|
||||
color: #888;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
@@ -279,17 +252,4 @@ onUnmounted(() => {
|
||||
.state-msg.error {
|
||||
color: #ff7875;
|
||||
}
|
||||
|
||||
|
||||
/* Legacy */
|
||||
.legacy-list {
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.line {
|
||||
margin-bottom: 4px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
@@ -1,19 +1,30 @@
|
||||
import { onMounted, onUnmounted } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { ref } from 'vue';
|
||||
import { gameSocket } from '../api/socket';
|
||||
import { useWorldStore } from '../stores/world';
|
||||
import { useUiStore } from '../stores/ui';
|
||||
import { useWorldStore } from './world';
|
||||
import { useUiStore } from './ui';
|
||||
import type { TickPayloadDTO } from '../types/api';
|
||||
|
||||
export function useGameSocket() {
|
||||
const worldStore = useWorldStore();
|
||||
const uiStore = useUiStore();
|
||||
|
||||
export const useSocketStore = defineStore('socket', () => {
|
||||
const isConnected = ref(false);
|
||||
const lastError = ref<string | null>(null);
|
||||
|
||||
let cleanupMessage: (() => void) | undefined;
|
||||
let cleanupStatus: (() => void) | undefined;
|
||||
|
||||
onMounted(() => {
|
||||
// Connect socket
|
||||
gameSocket.connect();
|
||||
function init() {
|
||||
if (cleanupStatus) return; // Already initialized
|
||||
|
||||
const worldStore = useWorldStore();
|
||||
const uiStore = useUiStore();
|
||||
|
||||
// Listen for status
|
||||
cleanupStatus = gameSocket.onStatusChange((connected) => {
|
||||
isConnected.value = connected;
|
||||
if (connected) {
|
||||
lastError.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for ticks
|
||||
cleanupMessage = gameSocket.on((data: any) => {
|
||||
@@ -27,25 +38,30 @@ export function useGameSocket() {
|
||||
uiStore.clearHoverCache();
|
||||
|
||||
// Refresh Detail if open (Silent update)
|
||||
// 注意:这里可以选择是否每次 tick 都刷新详情,或者让用户手动刷新
|
||||
// 为了实时性,通常会尝试静默刷新
|
||||
if (uiStore.selectedTarget) {
|
||||
uiStore.refreshDetail();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for status
|
||||
cleanupStatus = gameSocket.onStatusChange((connected) => {
|
||||
console.log('Socket status:', connected ? 'Connected' : 'Disconnected');
|
||||
// Could update a connection status in a store if needed
|
||||
});
|
||||
});
|
||||
// Connect socket
|
||||
gameSocket.connect();
|
||||
}
|
||||
|
||||
onUnmounted(() => {
|
||||
function disconnect() {
|
||||
if (cleanupMessage) cleanupMessage();
|
||||
if (cleanupStatus) cleanupStatus();
|
||||
cleanupMessage = undefined;
|
||||
cleanupStatus = undefined;
|
||||
gameSocket.disconnect();
|
||||
});
|
||||
}
|
||||
isConnected.value = false;
|
||||
}
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
lastError,
|
||||
init,
|
||||
disconnect
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ref, shallowRef, computed } from 'vue';
|
||||
import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary, CelestialPhenomenon } from '../types/core';
|
||||
import type { TickPayloadDTO, InitialStateDTO } from '../types/api';
|
||||
import { gameApi } from '../api/game';
|
||||
import { processNewEvents, mergeAndSortEvents } from '../utils/eventHelper';
|
||||
|
||||
export const useWorldStore = defineStore('world', () => {
|
||||
// --- State ---
|
||||
@@ -65,50 +66,8 @@ export const useWorldStore = defineStore('world', () => {
|
||||
function addEvents(rawEvents: any[]) {
|
||||
if (!rawEvents || rawEvents.length === 0) return;
|
||||
|
||||
// 转换 DTO -> Domain
|
||||
// 增加临时索引 _seq 记录原始逻辑顺序,用于同时间戳事件的排序
|
||||
const newEvents: GameEvent[] = rawEvents.map((e, index) => ({
|
||||
id: e.id,
|
||||
text: e.text,
|
||||
content: e.content,
|
||||
year: e.year ?? year.value,
|
||||
month: e.month ?? month.value,
|
||||
timestamp: (e.year ?? year.value) * 12 + (e.month ?? month.value),
|
||||
relatedAvatarIds: e.related_avatar_ids || [],
|
||||
isMajor: e.is_major,
|
||||
isStory: e.is_story,
|
||||
_seq: index
|
||||
} as GameEvent & { _seq: number }));
|
||||
|
||||
// 排序并保留最新的 N 条
|
||||
const MAX_EVENTS = 300;
|
||||
const combined = [...newEvents, ...events.value];
|
||||
|
||||
combined.sort((a, b) => {
|
||||
// 1. 先按时间戳升序(最旧的月在上面)
|
||||
const ta = a.timestamp;
|
||||
const tb = b.timestamp;
|
||||
if (tb !== ta) {
|
||||
return ta - tb;
|
||||
}
|
||||
|
||||
// 2. 时间相同时,按原始逻辑顺序升序(先发生的在上面)
|
||||
// 旧事件通常没有 _seq (undefined),视为最旧 (-1)
|
||||
const seqA = (a as any)._seq ?? -1;
|
||||
const seqB = (b as any)._seq ?? -1;
|
||||
|
||||
// 如果都是旧事件,保持相对顺序 (Stable)
|
||||
if (seqA === -1 && seqB === -1) return 0;
|
||||
|
||||
return seqA - seqB;
|
||||
});
|
||||
|
||||
// 保留最新的 N 条 (因为是升序,最新的在最后,所以取最后 N 条)
|
||||
if (combined.length > MAX_EVENTS) {
|
||||
events.value = combined.slice(-MAX_EVENTS);
|
||||
} else {
|
||||
events.value = combined;
|
||||
}
|
||||
const newEvents = processNewEvents(rawEvents, year.value, month.value);
|
||||
events.value = mergeAndSortEvents(events.value, newEvents);
|
||||
}
|
||||
|
||||
function handleTick(payload: TickPayloadDTO) {
|
||||
@@ -116,19 +75,6 @@ export const useWorldStore = defineStore('world', () => {
|
||||
|
||||
setTime(payload.year, payload.month);
|
||||
|
||||
// 检查并处理死亡事件,移除已死亡的角色
|
||||
// if (payload.events && Array.isArray(payload.events)) {
|
||||
// const deathEvents = (payload.events as any[]).filter((e: any) => {
|
||||
// const c = e.content || '';
|
||||
// return c.includes('身亡') || c.includes('老死');
|
||||
// });
|
||||
//
|
||||
// if (deathEvents.length > 0) {
|
||||
// // 旧逻辑:主动删除死人。现在改为软删除,后端会在 avatars 更新中推送 is_dead 状态,
|
||||
// // 所以这里不再需要主动操作。前端展示层根据 is_dead 决定是否隐藏。
|
||||
// }
|
||||
// }
|
||||
|
||||
if (payload.avatars) updateAvatars(payload.avatars);
|
||||
if (payload.events) addEvents(payload.events);
|
||||
if (payload.phenomenon !== undefined) {
|
||||
|
||||
@@ -3,8 +3,20 @@
|
||||
-apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
line-height: 1.4;
|
||||
font-weight: 400;
|
||||
color: #f6f6f6;
|
||||
background-color: #050608;
|
||||
|
||||
/* Colors */
|
||||
--color-bg-dark: #050608;
|
||||
--color-text-main: #f6f6f6;
|
||||
--color-text-secondary: #888;
|
||||
--color-border: #333;
|
||||
|
||||
/* Panels */
|
||||
--panel-bg: rgba(24, 24, 24, 0.96);
|
||||
--panel-header-bg: rgba(38, 38, 38, 0.95);
|
||||
--panel-shadow: 0 10px 25px rgba(0, 0, 0, 0.45);
|
||||
|
||||
color: var(--color-text-main);
|
||||
background-color: var(--color-bg-dark);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
|
||||
@@ -171,6 +171,9 @@ export interface GameEvent {
|
||||
relatedAvatarIds: string[];
|
||||
isMajor: boolean;
|
||||
isStory: boolean;
|
||||
|
||||
// 运行时辅助字段
|
||||
_seq?: number;
|
||||
}
|
||||
|
||||
// --- 悬浮提示 (Hover) ---
|
||||
|
||||
60
web/src/utils/eventHelper.ts
Normal file
60
web/src/utils/eventHelper.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import type { GameEvent } from '../types/core';
|
||||
|
||||
export const MAX_EVENTS = 300;
|
||||
|
||||
/**
|
||||
* 处理新事件列表,转换为 Domain 对象并分配序列号
|
||||
*/
|
||||
export function processNewEvents(rawEvents: any[], currentYear: number, currentMonth: number): GameEvent[] {
|
||||
if (!rawEvents || rawEvents.length === 0) return [];
|
||||
|
||||
return rawEvents.map((e, index) => ({
|
||||
id: e.id,
|
||||
text: e.text,
|
||||
content: e.content,
|
||||
year: e.year ?? currentYear,
|
||||
month: e.month ?? currentMonth,
|
||||
timestamp: (e.year ?? currentYear) * 12 + (e.month ?? currentMonth),
|
||||
relatedAvatarIds: e.related_avatar_ids || [],
|
||||
isMajor: e.is_major,
|
||||
isStory: e.is_story,
|
||||
_seq: index
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并并排序事件列表
|
||||
* 1. 按时间戳升序
|
||||
* 2. 时间戳相同时,按序列号升序
|
||||
* 3. 保留最新的 MAX_EVENTS 条
|
||||
*/
|
||||
export function mergeAndSortEvents(existingEvents: GameEvent[], newEvents: GameEvent[]): GameEvent[] {
|
||||
const combined = [...newEvents, ...existingEvents];
|
||||
|
||||
combined.sort((a, b) => {
|
||||
// 1. 先按时间戳升序(最旧的月在上面)
|
||||
const ta = a.timestamp;
|
||||
const tb = b.timestamp;
|
||||
if (tb !== ta) {
|
||||
return ta - tb;
|
||||
}
|
||||
|
||||
// 2. 时间相同时,按原始逻辑顺序升序(先发生的在上面)
|
||||
// 旧事件通常没有 _seq (undefined),视为最旧 (-1)
|
||||
const seqA = a._seq ?? -1;
|
||||
const seqB = b._seq ?? -1;
|
||||
|
||||
// 如果都是旧事件,保持相对顺序 (Stable)
|
||||
if (seqA === -1 && seqB === -1) return 0;
|
||||
|
||||
return seqA - seqB;
|
||||
});
|
||||
|
||||
// 保留最新的 N 条 (因为是升序,最新的在最后,所以取最后 N 条)
|
||||
if (combined.length > MAX_EVENTS) {
|
||||
return combined.slice(-MAX_EVENTS);
|
||||
}
|
||||
|
||||
return combined;
|
||||
}
|
||||
|
||||
53
web/src/utils/mapStyles.ts
Normal file
53
web/src/utils/mapStyles.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import type { TextStyleOptions } from 'pixi.js';
|
||||
|
||||
// 地图渲染相关的样式常量
|
||||
|
||||
export const REGION_STYLES: Record<string, Partial<TextStyleOptions>> = {
|
||||
sect: {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: 60,
|
||||
fill: '#ffcc00',
|
||||
stroke: { color: '#000000', width: 5, join: 'round' },
|
||||
align: 'center',
|
||||
dropShadow: {
|
||||
color: '#000000',
|
||||
blur: 3,
|
||||
angle: Math.PI / 6,
|
||||
distance: 3,
|
||||
alpha: 0.8
|
||||
}
|
||||
},
|
||||
city: {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: 72,
|
||||
fill: '#ccffcc',
|
||||
stroke: { color: '#000000', width: 5, join: 'round' },
|
||||
align: 'center',
|
||||
dropShadow: {
|
||||
color: '#000000',
|
||||
blur: 3,
|
||||
angle: Math.PI / 6,
|
||||
distance: 3,
|
||||
alpha: 0.8
|
||||
}
|
||||
},
|
||||
default: {
|
||||
fontFamily: '"Microsoft YaHei", sans-serif',
|
||||
fontSize: 72,
|
||||
fill: '#ffffff',
|
||||
stroke: { color: '#000000', width: 5, join: 'round' },
|
||||
align: 'center',
|
||||
dropShadow: {
|
||||
color: '#000000',
|
||||
blur: 3,
|
||||
angle: Math.PI / 6,
|
||||
distance: 3,
|
||||
alpha: 0.8
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function getRegionTextStyle(type: string): Partial<TextStyleOptions> {
|
||||
return REGION_STYLES[type] || REGION_STYLES.default;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,45 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import { compilerOptions } from 'vue3-pixi'
|
||||
import path from 'path'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions,
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
assetsDir: 'web_static', // 避免与游戏原本的 /assets 目录冲突
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8002',
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/ws': {
|
||||
target: 'ws://localhost:8002',
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/assets': {
|
||||
target: 'http://localhost:8002',
|
||||
changeOrigin: true,
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
const API_TARGET = env.VITE_API_TARGET || 'http://localhost:8002'
|
||||
const WS_TARGET = env.VITE_WS_TARGET || 'ws://localhost:8002'
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue({
|
||||
template: {
|
||||
compilerOptions,
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src')
|
||||
}
|
||||
},
|
||||
build: {
|
||||
assetsDir: 'web_static', // 避免与游戏原本的 /assets 目录冲突
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: API_TARGET,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/ws': {
|
||||
target: WS_TARGET,
|
||||
ws: true,
|
||||
changeOrigin: true,
|
||||
},
|
||||
'/assets': {
|
||||
target: API_TARGET,
|
||||
changeOrigin: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user