Merge remote-tracking branch 'origin/main' into fix/map-hover-title-parity-20260812
# Conflicts: # app/game-frontend/e2e/inGameInfo.spec.ts
This commit is contained in:
@@ -18,7 +18,7 @@ import { ConflictingTurnDaemonCommandError } from '../../daemon/databaseTranspor
|
||||
import { resolveAccessWindows } from '../../services/generalAccess.js';
|
||||
import { adjustAccountIconForUser } from '../../services/accountIconSync.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { resolveNationNotice } from '../nation/shared.js';
|
||||
import { loadTraitNames, resolveNationNotice, type TraitNameMap } from '../nation/shared.js';
|
||||
|
||||
const zGeneralSettings = z.object({
|
||||
tnmt: z.number().int().optional(),
|
||||
@@ -34,6 +34,17 @@ const zImmediateActionInput = z
|
||||
})
|
||||
.optional();
|
||||
const MAIN_RECORD_LIMIT = 15;
|
||||
const NEUTRAL_NATION_CONTEXT = {
|
||||
id: 0,
|
||||
name: '재야',
|
||||
color: '#000000',
|
||||
level: 0,
|
||||
gold: 0,
|
||||
rice: 0,
|
||||
tech: 0,
|
||||
typeCode: 'None',
|
||||
capitalCityId: null,
|
||||
} as const;
|
||||
|
||||
const resolveImmediateActionRequestId = (
|
||||
contextRequestId: string | undefined,
|
||||
@@ -135,6 +146,18 @@ const normalizeItemCode = (value: string | null): string | null => {
|
||||
return value;
|
||||
};
|
||||
|
||||
const resolveTraitDisplayName = (code: string, names: TraitNameMap): string => {
|
||||
if (!code || code === 'None') {
|
||||
return '-';
|
||||
}
|
||||
const loadedName = names.get(code)?.name;
|
||||
if (loadedName) {
|
||||
return loadedName;
|
||||
}
|
||||
// Ref는 class getName()을 표시하므로 로더가 모르는 선택적 특기도 raw namespace는 노출하지 않는다.
|
||||
return code.replace(/^che_(?:event_)?/u, '');
|
||||
};
|
||||
|
||||
const resolveUserSettings = (meta: Record<string, unknown>) => {
|
||||
// The legacy general columns are persisted at the top level of General.meta.
|
||||
// Keep reading the short-lived nested shape for installations that ran the
|
||||
@@ -265,10 +288,16 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
capitalCityId: true,
|
||||
},
|
||||
})
|
||||
: null,
|
||||
: Promise.resolve(NEUTRAL_NATION_CONTEXT),
|
||||
ctx.db.worldState.findFirst({ select: { config: true } }),
|
||||
]);
|
||||
|
||||
const [personalityNames, domesticNames, warNames] = await Promise.all([
|
||||
loadTraitNames([general.personalCode], 'personality'),
|
||||
loadTraitNames([general.specialCode], 'domestic'),
|
||||
loadTraitNames([general.special2Code], 'war'),
|
||||
]);
|
||||
|
||||
const metaRecord = asRecord(general.meta);
|
||||
const worldConfig = asRecord(worldState?.config);
|
||||
const constValues = asRecord(worldConfig.const ?? worldConfig.consts);
|
||||
@@ -303,9 +332,9 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
turnTime: general.turnTime.toISOString(),
|
||||
crewTypeId: general.crewTypeId,
|
||||
traits: {
|
||||
personal: general.personalCode,
|
||||
specialWar: general.specialCode,
|
||||
specialDomestic: general.special2Code,
|
||||
personal: resolveTraitDisplayName(general.personalCode, personalityNames),
|
||||
specialDomestic: resolveTraitDisplayName(general.specialCode, domesticNames),
|
||||
specialWar: resolveTraitDisplayName(general.special2Code, warNames),
|
||||
},
|
||||
progression: {
|
||||
experienceLevel: readNumber(metaRecord.explevel, 0),
|
||||
|
||||
@@ -248,6 +248,40 @@ describe('in-game my information ownership', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the Ref-style neutral nation frame and trait display names on the main read model', async () => {
|
||||
const fixture = createContext({
|
||||
me: buildGeneral({
|
||||
nationId: 0,
|
||||
officerLevel: 0,
|
||||
personalCode: 'che_안전',
|
||||
specialCode: 'che_상재',
|
||||
special2Code: 'che_신산',
|
||||
}),
|
||||
});
|
||||
|
||||
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
||||
general: {
|
||||
traits: {
|
||||
personal: '안전',
|
||||
specialDomestic: '상재',
|
||||
specialWar: '신산',
|
||||
},
|
||||
},
|
||||
nation: {
|
||||
id: 0,
|
||||
name: '재야',
|
||||
color: '#000000',
|
||||
level: 0,
|
||||
gold: 0,
|
||||
rice: 0,
|
||||
tech: 0,
|
||||
typeCode: 'None',
|
||||
capitalCityId: null,
|
||||
},
|
||||
});
|
||||
expect(fixture.db.nation.findUnique).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('reads legacy top-level settings and dispatches only the session-owned general', async () => {
|
||||
const requestCommand = vi.fn(async () => ({ type: 'setMySetting', ok: true, generalId: 7 }));
|
||||
const fixture = createContext({ requestCommand });
|
||||
|
||||
@@ -135,6 +135,7 @@ const install = async (
|
||||
page: Page,
|
||||
mode: 'member' | 'wanderer' | 'admin' = 'member',
|
||||
trade: number | null = 100,
|
||||
globalNationCount = 2,
|
||||
mapFixture = map
|
||||
) => {
|
||||
await page.addInitScript((profile) => {
|
||||
@@ -265,7 +266,7 @@ const install = async (
|
||||
generalCount: 1,
|
||||
cities: ['허창'],
|
||||
},
|
||||
],
|
||||
].slice(0, globalNationCount),
|
||||
diplomacy: { 1: { 1: 2, 2: 0 }, 2: { 1: 0, 2: 2 } },
|
||||
conflict: [],
|
||||
map: mapFixture,
|
||||
@@ -450,6 +451,26 @@ test('global-info renders the ref nation summary columns beside the map', async
|
||||
await page.screenshot({ path: resolve(artifactRoot, 'core-global-info-city-hover.png'), fullPage: true });
|
||||
}
|
||||
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [matrixWrapBox, matrixBox] = await Promise.all([
|
||||
page.locator('.matrix-wrap').boundingBox(),
|
||||
page.locator('.matrix').boundingBox(),
|
||||
]);
|
||||
if (!matrixWrapBox || !matrixBox) return null;
|
||||
return Math.abs(matrixWrapBox.height - matrixBox.height);
|
||||
})
|
||||
.toBeLessThan(1);
|
||||
|
||||
await expect
|
||||
.poll(() =>
|
||||
page
|
||||
.locator('.map-area .city-icon')
|
||||
.evaluateAll((images: HTMLImageElement[]) =>
|
||||
images.every((image) => image.complete && image.naturalWidth > 0)
|
||||
)
|
||||
)
|
||||
.toBe(true);
|
||||
const castleGeometry = await page.locator('.map-area').evaluate((mapArea) => {
|
||||
const mapRect = mapArea.getBoundingClientRect();
|
||||
return Array.from(mapArea.querySelectorAll<HTMLImageElement>('.city-icon')).map((image) => {
|
||||
@@ -595,7 +616,7 @@ test('global-info renders the ref nation summary columns beside the map', async
|
||||
});
|
||||
|
||||
test('map title keeps the ref early-game restriction boundary and color', async ({ page }) => {
|
||||
await install(page, 'member', 100, { ...map, startYear: 198 });
|
||||
await install(page, 'member', 100, 2, { ...map, startYear: 198 });
|
||||
await page.setViewportSize({ width: 1200, height: 900 });
|
||||
await go(page, 'global-info');
|
||||
|
||||
@@ -607,6 +628,62 @@ test('map title keeps the ref early-game restriction boundary and color', async
|
||||
);
|
||||
});
|
||||
|
||||
test('global-info diplomacy height follows the active nation count', async ({ page }) => {
|
||||
await install(page, 'member', 100, 1);
|
||||
await go(page, 'global-info');
|
||||
|
||||
const matrixWrap = page.locator('.matrix-wrap');
|
||||
const matrix = page.locator('.matrix');
|
||||
const mapSection = page.locator('.map-section');
|
||||
await expect(matrix.locator('tbody tr')).toHaveCount(1);
|
||||
|
||||
for (const viewport of [
|
||||
{ name: 'desktop', width: 1200, height: 900 },
|
||||
{ name: 'mobile', width: 390, height: 844 },
|
||||
]) {
|
||||
await page.setViewportSize(viewport);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const [wrapBox, matrixBox] = await Promise.all([matrixWrap.boundingBox(), matrix.boundingBox()]);
|
||||
if (!wrapBox || !matrixBox) return null;
|
||||
return Math.abs(wrapBox.height - matrixBox.height);
|
||||
})
|
||||
.toBeLessThan(1);
|
||||
|
||||
const geometry = await page.evaluate(() => {
|
||||
const rect = (selector: string) => document.querySelector(selector)?.getBoundingClientRect();
|
||||
const diplomacy = rect('.section');
|
||||
const matrix = rect('.matrix');
|
||||
const matrixWrap = rect('.matrix-wrap');
|
||||
const mapSection = rect('.map-section');
|
||||
return {
|
||||
diplomacyHeight: diplomacy?.height ?? null,
|
||||
matrixHeight: matrix?.height ?? null,
|
||||
matrixWrapHeight: matrixWrap?.height ?? null,
|
||||
gapToMap: matrixWrap && mapSection ? mapSection.top - matrixWrap.bottom : null,
|
||||
};
|
||||
});
|
||||
expect(geometry.matrixHeight).not.toBeNull();
|
||||
expect(geometry.matrixWrapHeight).toBeCloseTo(geometry.matrixHeight!, 0);
|
||||
expect(geometry.diplomacyHeight).toBeLessThan(200);
|
||||
expect(geometry.gapToMap).toBe(21);
|
||||
await expect(mapSection).toBeInViewport();
|
||||
|
||||
if (artifactRoot) {
|
||||
await mkdir(artifactRoot, { recursive: true });
|
||||
await writeFile(
|
||||
resolve(artifactRoot, `core-global-info-one-nation-${viewport.name}.json`),
|
||||
`${JSON.stringify(geometry, null, 2)}\n`,
|
||||
'utf8'
|
||||
);
|
||||
await page.screenshot({
|
||||
path: resolve(artifactRoot, `core-global-info-one-nation-${viewport.name}.png`),
|
||||
fullPage: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
test('current-city hides values and general rows for a wandering user', async ({ page }) => {
|
||||
await install(page, 'wanderer');
|
||||
await go(page, 'current-city');
|
||||
|
||||
@@ -45,6 +45,7 @@ type FixtureState = {
|
||||
adjustIconInputs?: Array<Record<string, unknown>>;
|
||||
joinConfig?: Record<string, unknown>;
|
||||
createGeneralInputs?: Array<Record<string, unknown>>;
|
||||
mainTraits?: { personal: string; specialDomestic: string; specialWar: string };
|
||||
};
|
||||
|
||||
type TrpcRequestPayload = {
|
||||
@@ -75,7 +76,7 @@ const myGeneral = (state: FixtureState) => ({
|
||||
age: 30,
|
||||
turnTime: '2026-01-01 00:10:00',
|
||||
crewTypeId: 1,
|
||||
traits: { personal: 'None', specialDomestic: 'None', specialWar: 'None' },
|
||||
traits: state.mainTraits ?? { personal: '-', specialDomestic: '-', specialWar: '-' },
|
||||
progression: {
|
||||
experienceLevel: 1,
|
||||
dedicationLevel: 2,
|
||||
@@ -86,7 +87,19 @@ const myGeneral = (state: FixtureState) => ({
|
||||
items: { horse: 'che_명마', weapon: null, book: null, item: null },
|
||||
},
|
||||
city: { id: 1, name: '업', level: 8, nationId: 1 },
|
||||
nation: { id: 1, name: '위', color: '#777777', level: 3 },
|
||||
nation: state.buildNationCandidateEnabled
|
||||
? {
|
||||
id: 0,
|
||||
name: '재야',
|
||||
color: '#000000',
|
||||
level: 0,
|
||||
gold: 0,
|
||||
rice: 0,
|
||||
tech: 0,
|
||||
typeCode: 'None',
|
||||
capitalCityId: null,
|
||||
}
|
||||
: { id: 1, name: '위', color: '#777777', level: 3 },
|
||||
settings: {
|
||||
tnmt: 0,
|
||||
defence_train: 80,
|
||||
@@ -221,6 +234,25 @@ const install = async (page: Page, state: FixtureState) => {
|
||||
state.createGeneralInputs?.push(jsonInput);
|
||||
return response({ generalId: 9 });
|
||||
}
|
||||
if (operation === 'dashboard.getContextBundleDelta') {
|
||||
return response({
|
||||
context: {
|
||||
kind: 'snapshot',
|
||||
revision: 'AAAAAAAAAAAAAAAAAAAAAA',
|
||||
data: myGeneral(state),
|
||||
},
|
||||
commandTable: {
|
||||
kind: 'snapshot',
|
||||
revision: 'BBBBBBBBBBBBBBBBBBBBBB',
|
||||
data: { general: [], nation: [] },
|
||||
},
|
||||
boardAccess: {
|
||||
kind: 'snapshot',
|
||||
revision: 'CCCCCCCCCCCCCCCCCCCCCC',
|
||||
data: { permission: 4, canMeeting: true, canSecret: true },
|
||||
},
|
||||
});
|
||||
}
|
||||
if (operation === 'general.me') {
|
||||
state.generalMeQueries = (state.generalMeQueries ?? 0) + 1;
|
||||
return response(myGeneral(state));
|
||||
@@ -437,6 +469,49 @@ test('정화된 국가 방침은 실행 가능한 속성 없이 Chromium에 표
|
||||
.toBeUndefined();
|
||||
});
|
||||
|
||||
test('재야 메인은 국가 틀과 성격·특기 표기명을 Chromium에 표시한다', async ({ page }) => {
|
||||
const state: FixtureState = {
|
||||
permission: 'member',
|
||||
myset: 0,
|
||||
buildNationCandidateEnabled: true,
|
||||
mainTraits: { personal: '안전', specialDomestic: '상재', specialWar: '신산' },
|
||||
settingMutations: [],
|
||||
accessPages: [],
|
||||
};
|
||||
await install(page, state);
|
||||
await page.setViewportSize({ width: 1000, height: 900 });
|
||||
await page.goto('');
|
||||
|
||||
const nationCard = page.locator('.nation-card');
|
||||
await expect(nationCard.locator('.title')).toHaveText('재야');
|
||||
await expect(nationCard.locator('.empty')).toHaveCount(0);
|
||||
await expect(nationCard.locator('.grid strong')).toHaveText(Array.from({ length: 6 }, () => '해당 없음'));
|
||||
|
||||
const generalCard = page.locator('.general-card');
|
||||
await expect(generalCard).toContainText('성격안전');
|
||||
await expect(generalCard).toContainText('전투특기신산');
|
||||
await expect(generalCard).toContainText('내정특기상재');
|
||||
await expect(generalCard).not.toContainText('che_');
|
||||
|
||||
const geometry = await nationCard.evaluate((element) => {
|
||||
const rect = element.getBoundingClientRect();
|
||||
const title = element.querySelector<HTMLElement>('.title')!;
|
||||
return {
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
titleBackground: getComputedStyle(title).backgroundColor,
|
||||
placeholderCount: [...element.querySelectorAll('.grid strong')].filter(
|
||||
(cell) => cell.textContent?.trim() === '해당 없음'
|
||||
).length,
|
||||
};
|
||||
});
|
||||
expect(geometry.width).toBeGreaterThan(0);
|
||||
expect(geometry.height).toBeGreaterThan(0);
|
||||
expect(geometry.titleBackground).toBe('rgb(0, 0, 0)');
|
||||
expect(geometry.placeholderCount).toBe(6);
|
||||
await persistParityArtifact(page, 'main-neutral-trait-display', geometry);
|
||||
});
|
||||
|
||||
test('접속량정보 keeps the legacy public 1016px chart geometry', async ({ page }) => {
|
||||
const state: FixtureState = { permission: 'member', myset: 0, settingMutations: [], accessPages: [] };
|
||||
await install(page, state);
|
||||
|
||||
@@ -31,15 +31,19 @@ const props = defineProps<{
|
||||
class="title"
|
||||
:style="{ backgroundColor: props.nation.color, color: legacyNationTextColor(props.nation.color) }"
|
||||
>
|
||||
{{ props.nation.name }} (Lv {{ props.nation.level }})
|
||||
{{ props.nation.name }}<template v-if="props.nation.id > 0"> (Lv {{ props.nation.level }})</template>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<span>국고</span><strong>{{ props.nation.gold.toLocaleString() }}</strong> <span>국량</span
|
||||
><strong>{{ props.nation.rice.toLocaleString() }}</strong> <span>기술</span
|
||||
><strong>{{ props.nation.tech.toLocaleString() }}</strong> <span>체제</span
|
||||
><strong>{{ props.nation.typeCode }}</strong> <span>수도</span
|
||||
><strong>{{ props.nation.capitalCityId ?? '-' }}</strong> <span>국가 등급</span
|
||||
><strong>{{ props.nation.level }}</strong>
|
||||
<span>국고</span
|
||||
><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.gold.toLocaleString() }}</strong>
|
||||
<span>국량</span
|
||||
><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.rice.toLocaleString() }}</strong>
|
||||
<span>기술</span
|
||||
><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.tech.toLocaleString() }}</strong>
|
||||
<span>체제</span><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.typeCode }}</strong>
|
||||
<span>수도</span
|
||||
><strong>{{ props.nation.id === 0 ? '해당 없음' : (props.nation.capitalCityId ?? '-') }}</strong>
|
||||
<span>국가 등급</span><strong>{{ props.nation.id === 0 ? '해당 없음' : props.nation.level }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import MapViewer from '../components/main/MapViewer.vue';
|
||||
import { trpc } from '../utils/trpc';
|
||||
@@ -10,6 +10,8 @@ type Layout = Awaited<ReturnType<typeof trpc.world.getMapLayout.query>>;
|
||||
const data = ref<Result | null>(null);
|
||||
const layout = ref<Layout | null>(null);
|
||||
const error = ref('');
|
||||
const matrixElement = ref<HTMLTableElement | null>(null);
|
||||
const matrixHeight = ref<number | null>(null);
|
||||
const router = useRouter();
|
||||
const goBack = () => router.push('/');
|
||||
const state = (value: number) => ({ 0: '★', 1: '▲', 2: '', 7: '@' })[value] ?? 'ㆍ';
|
||||
@@ -19,6 +21,23 @@ const nationNameStyle = (color: string) => ({
|
||||
backgroundColor: color,
|
||||
color: legacyNationTextColor(color),
|
||||
});
|
||||
watch(
|
||||
matrixElement,
|
||||
(element, _previousElement, onCleanup) => {
|
||||
if (!element) {
|
||||
matrixHeight.value = null;
|
||||
return;
|
||||
}
|
||||
const updateHeight = () => {
|
||||
matrixHeight.value = element.getBoundingClientRect().height;
|
||||
};
|
||||
const observer = new ResizeObserver(updateHeight);
|
||||
observer.observe(element);
|
||||
updateHeight();
|
||||
onCleanup(() => observer.disconnect());
|
||||
},
|
||||
{ flush: 'post' }
|
||||
);
|
||||
onMounted(async () => {
|
||||
try {
|
||||
[data.value, layout.value] = await Promise.all([
|
||||
@@ -39,8 +58,8 @@ onMounted(async () => {
|
||||
<p v-if="error" class="error">{{ error }}</p>
|
||||
<section v-if="data" class="section">
|
||||
<h2 class="blue">외교 현황</h2>
|
||||
<div class="matrix-wrap">
|
||||
<table class="matrix">
|
||||
<div class="matrix-wrap" :style="{ height: matrixHeight === null ? undefined : `${matrixHeight}px` }">
|
||||
<table ref="matrixElement" class="matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
@@ -194,7 +213,6 @@ onMounted(async () => {
|
||||
background: green;
|
||||
}
|
||||
.matrix-wrap {
|
||||
height: 1212.5px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ const history = ref<HistoryData | null>(null);
|
||||
const selectedYearMonth = ref<number | null>(null);
|
||||
const settingsOpen = ref(false);
|
||||
const rankingBottom = ref(localStorage.getItem('yearbook-ranking-bottom') === 'true');
|
||||
let historyRequestId = 0;
|
||||
const serverID = computed(() => {
|
||||
const value = route.query.serverID;
|
||||
const raw = Array.isArray(value) ? value[0] : value;
|
||||
@@ -71,19 +72,27 @@ const loadHistory = async (): Promise<void> => {
|
||||
if (selectedYearMonth.value === null) {
|
||||
return;
|
||||
}
|
||||
const requestId = ++historyRequestId;
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
const { year, month } = parseYearMonth(selectedYearMonth.value);
|
||||
const result = await trpc.yearbook.getHistory.query({ year, month, serverID: serverID.value });
|
||||
if (requestId !== historyRequestId) {
|
||||
return;
|
||||
}
|
||||
if ('data' in result) {
|
||||
history.value = result.data;
|
||||
}
|
||||
} catch (error) {
|
||||
history.value = null;
|
||||
if (requestId !== historyRequestId) {
|
||||
return;
|
||||
}
|
||||
errorMessage.value = error instanceof Error ? error.message : '연감 데이터를 불러오지 못했습니다.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
if (requestId === historyRequestId) {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -129,7 +138,13 @@ onMounted(async () => {
|
||||
<strong>연 감</strong>
|
||||
<button class="legacy-button close-button" type="button" @click="closePage">창 닫기</button>
|
||||
<span class="settings-menu">
|
||||
<button class="legacy-button legacy-button--navigation" type="button" @click="settingsOpen = !settingsOpen">⚙ 설정⌄</button>
|
||||
<button
|
||||
class="legacy-button legacy-button--navigation"
|
||||
type="button"
|
||||
@click="settingsOpen = !settingsOpen"
|
||||
>
|
||||
⚙ 설정⌄
|
||||
</button>
|
||||
<button v-if="settingsOpen" class="settings-item" type="button" @click="toggleRankingPosition">
|
||||
국가 순서 위치 변경(모바일 전용)
|
||||
</button>
|
||||
@@ -164,9 +179,9 @@ onMounted(async () => {
|
||||
<div v-if="errorMessage" class="yearbook-message error" role="alert">{{ errorMessage }}</div>
|
||||
<div v-else-if="loading && !history" class="yearbook-message">불러오는 중...</div>
|
||||
|
||||
<section v-if="history" :class="['history-grid', { 'ranking-bottom': rankingBottom }]">
|
||||
<section v-if="history" :class="['history-grid', { 'ranking-bottom': rankingBottom }]" :aria-busy="loading">
|
||||
<div class="map-position">
|
||||
<MapViewer :map-data="history.map" :map-layout="mapLayout" :loading="loading" />
|
||||
<MapViewer :map-data="history.map" :map-layout="mapLayout" :loading="loading && !history" />
|
||||
</div>
|
||||
<aside class="nation-position">
|
||||
<table>
|
||||
|
||||
Reference in New Issue
Block a user