refactor frontend

This commit is contained in:
bridge
2025-12-04 21:33:14 +08:00
parent 880e83c53e
commit ef0ff24783
11 changed files with 253 additions and 219 deletions

View File

@@ -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)"

View File

@@ -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>