fix: restore ref map hover details and title
This commit is contained in:
@@ -10,6 +10,11 @@ export type BaseMapResult = {
|
|||||||
startYear: number;
|
startYear: number;
|
||||||
year: number;
|
year: number;
|
||||||
month: number;
|
month: number;
|
||||||
|
techLevelLimit: {
|
||||||
|
maxLevel: number;
|
||||||
|
initialLevel: number;
|
||||||
|
increaseYears: number;
|
||||||
|
};
|
||||||
cityList: MapCityCompact[];
|
cityList: MapCityCompact[];
|
||||||
nationList: MapNationCompact[];
|
nationList: MapNationCompact[];
|
||||||
};
|
};
|
||||||
@@ -64,6 +69,23 @@ const readState = (meta: Record<string, unknown>): number => {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readPositiveInteger = (value: unknown, fallback: number): number => {
|
||||||
|
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
const normalized = Math.floor(value);
|
||||||
|
return normalized > 0 ? normalized : fallback;
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveTechLevelLimit = (worldState: WorldStateRow): BaseMapResult['techLevelLimit'] => {
|
||||||
|
const constValues = asRecord(asRecord(worldState.config).const);
|
||||||
|
return {
|
||||||
|
maxLevel: readPositiveInteger(constValues.maxTechLevel, 12),
|
||||||
|
initialLevel: readPositiveInteger(constValues.initialAllowedTechLevel, 1),
|
||||||
|
increaseYears: readPositiveInteger(constValues.techLevelIncYear, 5),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeNumberRecord = (value: unknown): Record<number, number> => {
|
const normalizeNumberRecord = (value: unknown): Record<number, number> => {
|
||||||
if (!isRecord(value)) {
|
if (!isRecord(value)) {
|
||||||
return {};
|
return {};
|
||||||
@@ -180,6 +202,7 @@ const loadBaseMap = async (
|
|||||||
startYear: resolveStartYear(worldState),
|
startYear: resolveStartYear(worldState),
|
||||||
year: worldState.currentYear,
|
year: worldState.currentYear,
|
||||||
month: worldState.currentMonth,
|
month: worldState.currentMonth,
|
||||||
|
techLevelLimit: resolveTechLevelLimit(worldState),
|
||||||
cityList,
|
cityList,
|
||||||
nationList,
|
nationList,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -26,7 +26,13 @@ const buildContext = () => {
|
|||||||
findFirst: vi.fn(async () => ({
|
findFirst: vi.fn(async () => ({
|
||||||
currentYear: 190,
|
currentYear: 190,
|
||||||
currentMonth: 3,
|
currentMonth: 3,
|
||||||
config: {},
|
config: {
|
||||||
|
const: {
|
||||||
|
maxTechLevel: 10,
|
||||||
|
initialAllowedTechLevel: 2,
|
||||||
|
techLevelIncYear: 4,
|
||||||
|
},
|
||||||
|
},
|
||||||
meta: { scenarioMeta: { startYear: 184 } },
|
meta: { scenarioMeta: { startYear: 184 } },
|
||||||
})),
|
})),
|
||||||
},
|
},
|
||||||
@@ -38,12 +44,8 @@ const buildContext = () => {
|
|||||||
},
|
},
|
||||||
$queryRaw: vi
|
$queryRaw: vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockResolvedValueOnce([
|
.mockResolvedValueOnce([{ id: 1, level: 5, nationId: 1, region: 1, supplyState: 1, meta: { state: 0 } }])
|
||||||
{ id: 1, level: 5, nationId: 1, region: 1, supplyState: 1, meta: { state: 0 } },
|
.mockResolvedValueOnce([{ id: 1, name: '촉', color: '#ff0000', capitalCityId: 1, meta: {} }]),
|
||||||
])
|
|
||||||
.mockResolvedValueOnce([
|
|
||||||
{ id: 1, name: '촉', color: '#ff0000', capitalCityId: 1, meta: {} },
|
|
||||||
]),
|
|
||||||
};
|
};
|
||||||
const context: GameApiContext = {
|
const context: GameApiContext = {
|
||||||
db: db as unknown as DatabaseClient,
|
db: db as unknown as DatabaseClient,
|
||||||
@@ -70,6 +72,11 @@ describe('public.getCachedMap', () => {
|
|||||||
expect(result).toMatchObject({
|
expect(result).toMatchObject({
|
||||||
year: 190,
|
year: 190,
|
||||||
month: 3,
|
month: 3,
|
||||||
|
techLevelLimit: {
|
||||||
|
maxLevel: 10,
|
||||||
|
initialLevel: 2,
|
||||||
|
increaseYears: 4,
|
||||||
|
},
|
||||||
history: [
|
history: [
|
||||||
{ id: 9, text: '<Y>최근 정세</>' },
|
{ id: 9, text: '<Y>최근 정세</>' },
|
||||||
{ id: 8, text: '이전 정세' },
|
{ id: 8, text: '이전 정세' },
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const map = {
|
|||||||
startYear: 180,
|
startYear: 180,
|
||||||
year: 200,
|
year: 200,
|
||||||
month: 1,
|
month: 1,
|
||||||
|
techLevelLimit: { maxLevel: 12, initialLevel: 1, increaseYears: 5 },
|
||||||
cityList: castleFixtures.map(({ id, level }) => [id, level, 0, 1, 1, 1]),
|
cityList: castleFixtures.map(({ id, level }) => [id, level, 0, 1, 1, 1]),
|
||||||
nationList: [[1, '아국', '#008000', 1]],
|
nationList: [[1, '아국', '#008000', 1]],
|
||||||
spyList: {},
|
spyList: {},
|
||||||
@@ -130,7 +131,12 @@ const emptyMessages = {
|
|||||||
canRespondDiplomacy: false,
|
canRespondDiplomacy: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
const install = async (page: Page, mode: 'member' | 'wanderer' | 'admin' = 'member', trade: number | null = 100) => {
|
const install = async (
|
||||||
|
page: Page,
|
||||||
|
mode: 'member' | 'wanderer' | 'admin' = 'member',
|
||||||
|
trade: number | null = 100,
|
||||||
|
mapFixture = map
|
||||||
|
) => {
|
||||||
await page.addInitScript((profile) => {
|
await page.addInitScript((profile) => {
|
||||||
localStorage.setItem('sammo-game-token', 'ga_info');
|
localStorage.setItem('sammo-game-token', 'ga_info');
|
||||||
localStorage.setItem('sammo-game-profile', profile);
|
localStorage.setItem('sammo-game-profile', profile);
|
||||||
@@ -143,13 +149,22 @@ const install = async (page: Page, mode: 'member' | 'wanderer' | 'admin' = 'memb
|
|||||||
body: await readImage(relativePath),
|
body: await readImage(relativePath),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
await page.route('**/game/**', async (route) => {
|
||||||
|
const relativePath = decodeURIComponent(new URL(route.request().url()).pathname.split('/game/')[1] ?? '');
|
||||||
|
const fixturePath = `game/${relativePath}`;
|
||||||
|
await route.fulfill({
|
||||||
|
status: 200,
|
||||||
|
contentType: imageContentType(fixturePath),
|
||||||
|
body: await readImage(fixturePath),
|
||||||
|
});
|
||||||
|
});
|
||||||
await page.route(`**${gameBasePath}/api/trpc/**`, async (route) => {
|
await page.route(`**${gameBasePath}/api/trpc/**`, async (route) => {
|
||||||
const results = operationNames(route).map((operation) => {
|
const results = operationNames(route).map((operation) => {
|
||||||
if (operation === 'auth.status') return response({ ok: true });
|
if (operation === 'auth.status') return response({ ok: true });
|
||||||
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '장수' } });
|
if (operation === 'lobby.info') return response({ myGeneral: { id: 1, name: '장수' } });
|
||||||
if (operation === 'join.getConfig') return response({});
|
if (operation === 'join.getConfig') return response({});
|
||||||
if (operation === 'general.me') return response(generalContext);
|
if (operation === 'general.me') return response(generalContext);
|
||||||
if (operation === 'world.getMap') return response(map);
|
if (operation === 'world.getMap') return response(mapFixture);
|
||||||
if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
|
if (operation === 'turns.getCommandTable') return response({ general: [], nation: [] });
|
||||||
if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
|
if (operation === 'turns.reserved.getGeneral' || operation === 'turns.reserved.getNation') {
|
||||||
return response({ turns: [], revision: 0 });
|
return response({ turns: [], revision: 0 });
|
||||||
@@ -253,7 +268,7 @@ const install = async (page: Page, mode: 'member' | 'wanderer' | 'admin' = 'memb
|
|||||||
],
|
],
|
||||||
diplomacy: { 1: { 1: 2, 2: 0 }, 2: { 1: 0, 2: 2 } },
|
diplomacy: { 1: { 1: 2, 2: 0 }, 2: { 1: 0, 2: 2 } },
|
||||||
conflict: [],
|
conflict: [],
|
||||||
map,
|
map: mapFixture,
|
||||||
});
|
});
|
||||||
if (operation === 'world.getMapLayout') return response(layout);
|
if (operation === 'world.getMapLayout') return response(layout);
|
||||||
if (operation === 'world.getCurrentCity')
|
if (operation === 'world.getCurrentCity')
|
||||||
@@ -375,6 +390,66 @@ test('global-info renders the ref nation summary columns beside the map', async
|
|||||||
await page.setViewportSize({ width: 1200, height: 900 });
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
await go(page, 'global-info');
|
await go(page, 'global-info');
|
||||||
|
|
||||||
|
const mapTitle = page.locator('.map-title');
|
||||||
|
await expect(mapTitle).toHaveText(/200年 1月/u);
|
||||||
|
const titleBackground = await page
|
||||||
|
.locator('.map-top')
|
||||||
|
.evaluate((element) => getComputedStyle(element).backgroundImage);
|
||||||
|
expect(titleBackground).toContain('ltitle.jpg');
|
||||||
|
expect(titleBackground).toContain('rtitle.jpg');
|
||||||
|
const titleTextBackground = await mapTitle.evaluate((element) => getComputedStyle(element).backgroundImage);
|
||||||
|
expect(titleTextBackground).toContain('ad.gif');
|
||||||
|
expect(titleTextBackground).toContain('spring.gif');
|
||||||
|
await mapTitle.hover();
|
||||||
|
const titleTooltip = page.locator('.map-title-tooltip');
|
||||||
|
await expect(titleTooltip).toBeVisible();
|
||||||
|
await expect(titleTooltip).toContainText('기술등급 제한 : 5등급 (205년 해제)');
|
||||||
|
const titleGeometry = await page.locator('.map-top').evaluate((element) => {
|
||||||
|
const band = element.getBoundingClientRect();
|
||||||
|
const title = element.querySelector('.map-title')?.getBoundingClientRect();
|
||||||
|
const tooltip = element.querySelector('.map-title-tooltip')?.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
band: { width: band.width, height: band.height },
|
||||||
|
title: title ? { width: title.width, height: title.height } : null,
|
||||||
|
tooltip: tooltip ? { width: tooltip.width, height: tooltip.height } : null,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(titleGeometry).toEqual({
|
||||||
|
band: { width: 700, height: 20 },
|
||||||
|
title: { width: 160, height: 20 },
|
||||||
|
tooltip: { width: 220, height: 28 },
|
||||||
|
});
|
||||||
|
if (artifactRoot) {
|
||||||
|
await mkdir(artifactRoot, { recursive: true });
|
||||||
|
await page.screenshot({ path: resolve(artifactRoot, 'core-global-info-title-hover.png'), fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hoveredCastle = page.locator('.city-base').first();
|
||||||
|
await hoveredCastle.hover();
|
||||||
|
const cityTooltip = page.locator('.map-tooltip');
|
||||||
|
await expect(cityTooltip).toBeVisible();
|
||||||
|
await expect(cityTooltip.locator('.tooltip-title')).toHaveText('【하북|특】업');
|
||||||
|
await expect(cityTooltip.locator('.tooltip-body')).toHaveText('아국');
|
||||||
|
const cityTooltipStyle = await cityTooltip.evaluate((element) => {
|
||||||
|
const style = getComputedStyle(element);
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
width: rect.width,
|
||||||
|
backgroundColor: style.backgroundColor,
|
||||||
|
fontSize: style.fontSize,
|
||||||
|
lineHeight: style.lineHeight,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(cityTooltipStyle.width).toBeGreaterThanOrEqual(120);
|
||||||
|
expect(cityTooltipStyle).toMatchObject({
|
||||||
|
backgroundColor: 'rgb(30, 164, 255)',
|
||||||
|
fontSize: '14px',
|
||||||
|
lineHeight: '15px',
|
||||||
|
});
|
||||||
|
if (artifactRoot) {
|
||||||
|
await page.screenshot({ path: resolve(artifactRoot, 'core-global-info-city-hover.png'), fullPage: true });
|
||||||
|
}
|
||||||
|
|
||||||
const castleGeometry = await page.locator('.map-area').evaluate((mapArea) => {
|
const castleGeometry = await page.locator('.map-area').evaluate((mapArea) => {
|
||||||
const mapRect = mapArea.getBoundingClientRect();
|
const mapRect = mapArea.getBoundingClientRect();
|
||||||
return Array.from(mapArea.querySelectorAll<HTMLImageElement>('.city-icon')).map((image) => {
|
return Array.from(mapArea.querySelectorAll<HTMLImageElement>('.city-icon')).map((image) => {
|
||||||
@@ -437,6 +512,10 @@ test('global-info renders the ref nation summary columns beside the map', async
|
|||||||
resolve(artifactRoot, 'core-global-info-computed-dom.json'),
|
resolve(artifactRoot, 'core-global-info-computed-dom.json'),
|
||||||
`${JSON.stringify(
|
`${JSON.stringify(
|
||||||
{
|
{
|
||||||
|
titleBackground,
|
||||||
|
titleTextBackground,
|
||||||
|
titleGeometry,
|
||||||
|
cityTooltipStyle,
|
||||||
geometry,
|
geometry,
|
||||||
castleGeometry,
|
castleGeometry,
|
||||||
headings: await summary.locator('th').allTextContents(),
|
headings: await summary.locator('th').allTextContents(),
|
||||||
@@ -489,11 +568,45 @@ test('global-info renders the ref nation summary columns beside the map', async
|
|||||||
expect(mobileGeometry.map?.width).toBe(500);
|
expect(mobileGeometry.map?.width).toBe(500);
|
||||||
expect(mobileGeometry.summary?.width).toBe(500);
|
expect(mobileGeometry.summary?.width).toBe(500);
|
||||||
expect(mobileGeometry.summary?.y).toBe(mobileGeometry.map?.bottom);
|
expect(mobileGeometry.summary?.y).toBe(mobileGeometry.map?.bottom);
|
||||||
|
await mapTitle.hover();
|
||||||
|
await expect(titleTooltip).toBeVisible();
|
||||||
|
const mobileTitleGeometry = await page.locator('.map-top').evaluate((element) => {
|
||||||
|
const band = element.getBoundingClientRect();
|
||||||
|
const tooltip = element.querySelector('.map-title-tooltip')?.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
band: { width: band.width, height: band.height },
|
||||||
|
tooltip: tooltip ? { x: tooltip.x, width: tooltip.width } : null,
|
||||||
|
documentWidth: document.documentElement.scrollWidth,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
expect(mobileTitleGeometry.band).toEqual({ width: 500, height: 20 });
|
||||||
|
expect(mobileTitleGeometry.tooltip?.width).toBe(220);
|
||||||
|
expect(mobileTitleGeometry.documentWidth).toBe(500);
|
||||||
|
await hoveredCastle.hover();
|
||||||
|
await expect(cityTooltip).toBeVisible();
|
||||||
|
await expect(cityTooltip.locator('.tooltip-title')).toHaveText('【하북|특】업');
|
||||||
if (artifactRoot) {
|
if (artifactRoot) {
|
||||||
|
await page.screenshot({
|
||||||
|
path: resolve(artifactRoot, 'core-global-info-mobile-city-hover.png'),
|
||||||
|
fullPage: true,
|
||||||
|
});
|
||||||
await page.screenshot({ path: resolve(artifactRoot, 'core-global-info-mobile.png'), fullPage: true });
|
await page.screenshot({ path: resolve(artifactRoot, 'core-global-info-mobile.png'), fullPage: true });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('map title keeps the ref early-game restriction boundary and color', async ({ page }) => {
|
||||||
|
await install(page, 'member', 100, { ...map, startYear: 198 });
|
||||||
|
await page.setViewportSize({ width: 1200, height: 900 });
|
||||||
|
await go(page, 'global-info');
|
||||||
|
|
||||||
|
const mapTitle = page.locator('.map-title');
|
||||||
|
await expect(mapTitle).toHaveCSS('color', 'rgb(255, 255, 0)');
|
||||||
|
await mapTitle.hover();
|
||||||
|
await expect(page.locator('.map-title-tooltip')).toHaveText(
|
||||||
|
'초반제한 기간 : 0년 12개월 (201년)기술등급 제한 : 1등급 (203년 해제)'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
test('current-city hides values and general rows for a wandering user', async ({ page }) => {
|
test('current-city hides values and general rows for a wandering user', async ({ page }) => {
|
||||||
await install(page, 'wanderer');
|
await install(page, 'wanderer');
|
||||||
await go(page, 'current-city');
|
await go(page, 'current-city');
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ interface MapSummary {
|
|||||||
year: number;
|
year: number;
|
||||||
month: number;
|
month: number;
|
||||||
startYear: number;
|
startYear: number;
|
||||||
|
techLevelLimit?: {
|
||||||
|
maxLevel: number;
|
||||||
|
initialLevel: number;
|
||||||
|
increaseYears: number;
|
||||||
|
};
|
||||||
cityList: [number, number, number, number, number, number][];
|
cityList: [number, number, number, number, number, number][];
|
||||||
nationList: [number, string, string, number][];
|
nationList: [number, string, string, number][];
|
||||||
myCity?: number | null;
|
myCity?: number | null;
|
||||||
@@ -198,9 +203,77 @@ const mapSummary = computed(() => {
|
|||||||
if (!props.mapData) {
|
if (!props.mapData) {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
return `${props.mapData.year}년 ${props.mapData.month}월`;
|
return `${props.mapData.year}年 ${props.mapData.month}月`;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const titleColor = computed(() => {
|
||||||
|
if (!props.mapData) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const { startYear, year } = props.mapData;
|
||||||
|
if (year < startYear + 1) {
|
||||||
|
return 'magenta';
|
||||||
|
}
|
||||||
|
if (year < startYear + 2) {
|
||||||
|
return 'orange';
|
||||||
|
}
|
||||||
|
if (year < startYear + 3) {
|
||||||
|
return 'yellow';
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleTooltipLines = computed(() => {
|
||||||
|
if (!props.mapData) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const { startYear, year, month } = props.mapData;
|
||||||
|
const lines: string[] = [];
|
||||||
|
if (year <= startYear + 3) {
|
||||||
|
// Ref uses joinYearMonth(startYear + 3, 0) as the limit boundary.
|
||||||
|
const remainingMonths = (startYear + 3) * 12 - 1 - (year * 12 + month - 1);
|
||||||
|
const remainYear = Math.trunc(remainingMonths / 12);
|
||||||
|
const remainMonth = (remainingMonths % 12) + 1;
|
||||||
|
lines.push(
|
||||||
|
`초반제한 기간 : ${remainYear}년${remainMonth > 0 ? ` ${remainMonth}개월` : ''} (${startYear + 3}년)`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = props.mapData.techLevelLimit ?? {
|
||||||
|
maxLevel: 12,
|
||||||
|
initialLevel: 1,
|
||||||
|
increaseYears: 5,
|
||||||
|
};
|
||||||
|
const currentLevel = Math.min(
|
||||||
|
limit.maxLevel,
|
||||||
|
Math.max(1, Math.floor((year - startYear) / limit.increaseYears) + limit.initialLevel)
|
||||||
|
);
|
||||||
|
if (currentLevel === limit.maxLevel) {
|
||||||
|
lines.push(`기술등급 제한 : ${currentLevel}등급 (최종)`);
|
||||||
|
} else {
|
||||||
|
lines.push(`기술등급 제한 : ${currentLevel}등급 (${currentLevel * limit.increaseYears + startYear}년 해제)`);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
});
|
||||||
|
|
||||||
|
const titleBandStyle = computed(() =>
|
||||||
|
detailMode.value
|
||||||
|
? {
|
||||||
|
backgroundImage: `url('${resolveAsset('ltitle.jpg')}'), url('${resolveAsset('rtitle.jpg')}')`,
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
);
|
||||||
|
|
||||||
|
const titleTextStyle = computed(() =>
|
||||||
|
detailMode.value
|
||||||
|
? {
|
||||||
|
color: titleColor.value,
|
||||||
|
backgroundImage: `url('${resolveAsset('ad.gif')}'), url('${resolveAsset(`${mapSeason.value}.gif`)}')`,
|
||||||
|
}
|
||||||
|
: { color: titleColor.value }
|
||||||
|
);
|
||||||
|
|
||||||
const mapThemeClass = computed(() => {
|
const mapThemeClass = computed(() => {
|
||||||
return `map-theme-${mapTheme.value}`;
|
return `map-theme-${mapTheme.value}`;
|
||||||
});
|
});
|
||||||
@@ -269,6 +342,24 @@ const hoveredCity = computed(() => {
|
|||||||
return cityViews.value.find((city) => city.id === hoveredCityId.value) ?? null;
|
return cityViews.value.find((city) => city.id === hoveredCityId.value) ?? null;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const hoveredCityTitle = computed(() => {
|
||||||
|
if (!hoveredCity.value) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return `【${hoveredCity.value.regionName}|${hoveredCity.value.levelName}】${hoveredCity.value.name}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const tooltipPosition = computed(() => {
|
||||||
|
const width = 120;
|
||||||
|
const offset = 10;
|
||||||
|
const mapPixelWidth = BASE_MAP_WIDTH * mapScale.value;
|
||||||
|
const left = elementX.value + width + offset > mapPixelWidth ? elementX.value - width - 5 : elementX.value + offset;
|
||||||
|
return {
|
||||||
|
left: `${Math.max(0, left)}px`,
|
||||||
|
top: `${elementY.value + 30}px`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const setHoveredCity = (cityId: number | null) => {
|
const setHoveredCity = (cityId: number | null) => {
|
||||||
mapStore.setHoveredCity(cityId);
|
mapStore.setHoveredCity(cityId);
|
||||||
};
|
};
|
||||||
@@ -280,8 +371,13 @@ const selectCity = (cityId: number) => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="map-viewer">
|
<div class="map-viewer">
|
||||||
<div class="map-top">
|
<div class="map-top" :style="titleBandStyle">
|
||||||
<div class="map-title">{{ mapSummary }}</div>
|
<div class="map-title" tabindex="0" :style="titleTextStyle">
|
||||||
|
{{ mapSummary }}
|
||||||
|
<div class="map-title-tooltip" role="tooltip">
|
||||||
|
<div v-for="line in titleTooltipLines" :key="line">{{ line }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="props.loading">
|
<div v-if="props.loading">
|
||||||
<SkeletonLines :lines="4" />
|
<SkeletonLines :lines="4" />
|
||||||
@@ -309,15 +405,9 @@ const selectCity = (cityId: number) => {
|
|||||||
@leave="setHoveredCity(null)"
|
@leave="setHoveredCity(null)"
|
||||||
@select="selectCity"
|
@select="selectCity"
|
||||||
/>
|
/>
|
||||||
<div
|
<div v-if="hoveredCity" class="map-tooltip" :style="tooltipPosition">
|
||||||
v-if="hoveredCity"
|
<div class="tooltip-title">{{ hoveredCityTitle }}</div>
|
||||||
class="map-tooltip"
|
<div class="tooltip-body">{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}</div>
|
||||||
:style="{ left: `${elementX + 16}px`, top: `${elementY + 16}px` }"
|
|
||||||
>
|
|
||||||
<div class="tooltip-title">{{ hoveredCity.name }}</div>
|
|
||||||
<div class="tooltip-body">
|
|
||||||
{{ hoveredCity.nationName }} · {{ hoveredCity.regionName }} · {{ hoveredCity.levelName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="map-controls">
|
<div class="map-controls">
|
||||||
<button class="map-toggle" :class="{ active: showCityName }" @click="mapStore.toggleCityName">
|
<button class="map-toggle" :class="{ active: showCityName }" @click="mapStore.toggleCityName">
|
||||||
@@ -338,17 +428,69 @@ const selectCity = (cityId: number) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.map-top {
|
.map-top {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 20px;
|
height: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #111;
|
background: #111;
|
||||||
|
background-position:
|
||||||
|
left top,
|
||||||
|
right top;
|
||||||
|
background-repeat: no-repeat;
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-title {
|
.map-title {
|
||||||
font-size: 0.95rem;
|
position: relative;
|
||||||
font-weight: 600;
|
display: block;
|
||||||
|
width: 160px;
|
||||||
|
height: 20px;
|
||||||
|
margin: auto;
|
||||||
|
background-position:
|
||||||
|
left top,
|
||||||
|
right top;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 20px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-title-tooltip {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 20;
|
||||||
|
bottom: calc(100% + 7px);
|
||||||
|
left: 50%;
|
||||||
|
display: none;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 220px;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 5px 8px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 18px;
|
||||||
|
text-align: left;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-title-tooltip::after {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 50%;
|
||||||
|
border: 5px solid transparent;
|
||||||
|
border-top-color: #000;
|
||||||
|
content: '';
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-title:hover .map-title-tooltip,
|
||||||
|
.map-title:focus .map-title-tooltip,
|
||||||
|
.map-title:focus-within .map-title-tooltip {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-controls {
|
.map-controls {
|
||||||
@@ -400,19 +542,28 @@ const selectCity = (cityId: number) => {
|
|||||||
|
|
||||||
.map-tooltip {
|
.map-tooltip {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
z-index: 16;
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 120px;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
border: 1px solid rgba(201, 164, 90, 0.4);
|
border: 1px solid gray;
|
||||||
background: rgba(16, 16, 16, 0.9);
|
padding: 0;
|
||||||
padding: 4px 6px;
|
background: rgb(30, 164, 255);
|
||||||
font-size: 0.65rem;
|
color: #fff;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 15px;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-title {
|
.tooltip-title {
|
||||||
font-weight: 600;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tooltip-body {
|
.tooltip-body {
|
||||||
color: rgba(232, 221, 196, 0.6);
|
height: 15px;
|
||||||
|
border-top: 1px solid gray;
|
||||||
|
color: #fff;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-empty {
|
.map-empty {
|
||||||
|
|||||||
Reference in New Issue
Block a user