Files
core2026/app/game-frontend/src/components/main/MapCityBasic.vue
T
Hide_D b621381109 feat: 지도 모바일 두 번 탭 이동을 복원
Ref처럼 첫 탭은 도시 툴팁만 표시하고 같은 도시의 두 번째 탭에서 이동하도록 한다. 한 번 탭 모드와 localStorage 보존, PC hover/click 회귀를 Chromium 테스트로 검증한다.
2026-08-21 00:58:57 +00:00

190 lines
4.4 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue';
import { RouterLink } from 'vue-router';
interface MapCityView {
id: number;
name: string;
level: number;
state: number;
stateClass: 'good' | 'bad' | 'war' | 'wrong';
nationName: string;
color: string;
x: number;
y: number;
isCapital: boolean;
isMyCity: boolean;
supply: boolean;
selected: boolean;
}
const props = defineProps<{
city: MapCityView;
showName: boolean;
mapScale: number;
selectOnly?: boolean;
readonly?: boolean;
}>();
const emit = defineEmits<{
(event: 'hover', cityId: number): void;
(event: 'leave'): void;
(event: 'touch', cityId: number, touchEvent: TouchEvent): void;
(event: 'touchleave'): void;
(event: 'select', cityId: number): void;
}>();
const size = computed(() => (6 + props.city.level * 2) * props.mapScale);
const stateSize = computed(() => 8 * props.mapScale);
const stateOffset = computed(() => -6 * props.mapScale);
const selectCity = () => {
if (!props.readonly) emit('select', props.city.id);
};
let touchOnTrack = false;
const touchstart = () => {
touchOnTrack = true;
};
const touchmove = () => {
touchOnTrack = false;
};
const touchend = (event: TouchEvent) => {
if (touchOnTrack) {
event.stopPropagation();
emit('touch', props.city.id, event);
return;
}
emit('touchleave');
};
</script>
<template>
<component
:is="props.readonly ? 'div' : props.selectOnly ? 'button' : RouterLink"
class="map-city"
:type="!props.readonly && props.selectOnly ? 'button' : undefined"
:to="
props.selectOnly || props.readonly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }
"
:class="[
`state-${props.city.stateClass}`,
{
mine: props.city.isMyCity,
selected: props.city.selected,
'supply-off': !props.city.supply,
readonly: props.readonly,
},
]"
:style="{ left: `${props.city.x}px`, top: `${props.city.y}px` }"
@mouseenter="emit('hover', props.city.id)"
@mouseleave="emit('leave')"
@touchstart="touchstart"
@touchmove="touchmove"
@touchend="touchend"
@click.stop="selectCity"
>
<div class="city-dot" :style="{ backgroundColor: props.city.color, width: `${size}px`, height: `${size}px` }">
<span v-if="props.city.isCapital" class="capital" />
</div>
<div
v-if="props.city.state > 0"
class="city-state"
:class="`state-${props.city.stateClass}`"
:style="{
width: `${stateSize}px`,
height: `${stateSize}px`,
left: `${stateOffset}px`,
top: `${stateOffset}px`,
}"
/>
<div v-if="props.showName" class="city-name">{{ props.city.name }}</div>
</component>
</template>
<style scoped>
.map-city {
position: absolute;
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
transform: translate(-50%, -50%);
font-size: 0.65rem;
color: rgba(232, 221, 196, 0.8);
cursor: pointer;
text-decoration: none;
padding: 0;
border: 0;
background: transparent;
}
.map-city.readonly {
cursor: default;
}
.city-dot {
border: 1px solid rgba(232, 221, 196, 0.6);
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
}
.capital {
width: 5px;
height: 5px;
background: rgba(232, 221, 196, 0.9);
}
.map-city.mine .city-dot {
box-shadow: 0 0 0 2px rgba(201, 164, 90, 0.6);
}
.map-city.selected .city-dot {
box-shadow: 0 0 0 2px rgba(255, 235, 150, 0.9);
}
.map-city.supply-off {
opacity: 0.6;
}
.map-city.state-good .city-dot {
border-color: rgba(120, 220, 120, 0.9);
}
.map-city.state-bad .city-dot {
border-color: rgba(240, 190, 90, 0.9);
}
.map-city.state-war .city-dot {
border-color: rgba(240, 90, 90, 0.9);
}
.map-city.state-wrong .city-dot {
border-color: rgba(150, 150, 150, 0.8);
}
.city-state {
position: absolute;
background: rgba(232, 221, 196, 0.8);
}
.city-state.state-war {
background: rgba(240, 90, 90, 0.9);
}
.city-state.state-bad {
background: rgba(240, 190, 90, 0.9);
}
.city-state.state-good {
background: rgba(90, 160, 255, 0.9);
}
.city-name {
white-space: nowrap;
}
</style>