Merge remote-tracking branch 'origin/main' into feature/game-action-feedback-20260812
This commit is contained in:
@@ -50,6 +50,7 @@ const commandArgsValid = ref(false);
|
||||
const expanded = ref(false);
|
||||
const menuRevision = ref(0);
|
||||
const pendingReservation = ref<CommandPatternEntry | null>(null);
|
||||
const collapsedRowCount = 15;
|
||||
|
||||
const loadStorage = (key: string) => {
|
||||
storage.value = new CommandStorage(key);
|
||||
@@ -108,7 +109,11 @@ const labelMap = computed(() => {
|
||||
for (const group of groups) for (const command of group.values) map.set(command.key, command.name);
|
||||
return map;
|
||||
});
|
||||
const displayRows = computed(() => props.rows.slice(0, expanded.value || props.compact ? props.rows.length : 14));
|
||||
const displayRows = computed(() =>
|
||||
props.rows.slice(0, expanded.value || props.compact ? props.rows.length : collapsedRowCount)
|
||||
);
|
||||
const quickPickerTop = computed(() => `${70 + (quickTarget.value ?? 0) * 34.4}px`);
|
||||
const rowLabel = (row: ReservedCommandRow): string => row.label ?? labelMap.value.get(row.action) ?? row.action;
|
||||
const selectedIndices = () => normalizedSelection(selected.value, previousSelected.value, props.rows.length);
|
||||
const pattern = () => extractPattern(props.rows, selectedIndices());
|
||||
const touchMenus = () => (menuRevision.value += 1);
|
||||
@@ -396,6 +401,87 @@ const clickOutsideMenu = (event: Event) => {
|
||||
</details>
|
||||
</aside>
|
||||
|
||||
<div v-if="editMode" class="advanced-actions">
|
||||
<details class="legacy-menu selected-menu">
|
||||
<summary>선택한 턴을</summary>
|
||||
<div class="menu-items">
|
||||
<button
|
||||
@click="
|
||||
cut();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
잘라내기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
copy();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
복사하기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
paste();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
붙여넣기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
textCopy();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
텍스트 복사
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
saveTemplate();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
보관하기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
repeatPattern();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
반복하기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
clearSelection();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
비우기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rearrange('pull');
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
지우고 당기기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rearrange('push');
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
뒤로 밀기
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
<button type="button" class="select-command" @click="openPicker()">명령 선택 ▾</button>
|
||||
</div>
|
||||
|
||||
<div class="queue-area">
|
||||
<div class="queue-grid" :class="{ advanced: editMode }">
|
||||
<DragSelect
|
||||
@@ -443,9 +529,11 @@ const clickOutsideMenu = (event: Event) => {
|
||||
<div
|
||||
v-for="row in displayRows"
|
||||
:key="row.index"
|
||||
:title="row.label ?? labelMap.get(row.action) ?? row.action"
|
||||
:title="row.autonomous ? `${rowLabel(row)} · 자율 행동` : rowLabel(row)"
|
||||
:class="{ autonomous: row.autonomous }"
|
||||
>
|
||||
{{ row.label ?? labelMap.get(row.action) ?? row.action }}
|
||||
<span>{{ rowLabel(row) }}</span>
|
||||
<small v-if="row.autonomous && row.action === '휴식'">(자율 행동)</small>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!editMode" class="edit-column">
|
||||
@@ -461,87 +549,6 @@ const clickOutsideMenu = (event: Event) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="editMode" class="advanced-actions">
|
||||
<details class="legacy-menu selected-menu">
|
||||
<summary>선택한 턴을</summary>
|
||||
<div class="menu-items">
|
||||
<button
|
||||
@click="
|
||||
cut();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
잘라내기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
copy();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
복사하기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
paste();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
붙여넣기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
textCopy();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
텍스트 복사
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
saveTemplate();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
보관하기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
repeatPattern();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
반복하기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
clearSelection();
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
비우기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rearrange('pull');
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
지우고 당기기
|
||||
</button>
|
||||
<button
|
||||
@click="
|
||||
rearrange('push');
|
||||
clickOutsideMenu($event);
|
||||
"
|
||||
>
|
||||
뒤로 밀기
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
<button type="button" class="select-command" @click="openPicker()">명령 선택 ▾</button>
|
||||
</div>
|
||||
|
||||
<div v-if="!props.compact" class="bottom-actions">
|
||||
<button type="button" @click="emit('shift', -1)">당기기</button>
|
||||
<button type="button" @click="emit('shift', 1)">미루기</button>
|
||||
@@ -550,7 +557,12 @@ const clickOutsideMenu = (event: Event) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="pickerOpen" class="command-picker" data-testid="command-picker">
|
||||
<div
|
||||
v-if="pickerOpen"
|
||||
class="command-picker"
|
||||
data-testid="command-picker"
|
||||
:style="quickTarget === null || props.compact ? undefined : { top: quickPickerTop }"
|
||||
>
|
||||
<header>
|
||||
<strong>{{ quickTarget === null ? '선택한 턴' : `${quickTarget + 1}턴` }} 명령 입력</strong
|
||||
><button type="button" aria-label="명령 입력 닫기" @click="closePicker">×</button>
|
||||
@@ -628,6 +640,9 @@ const clickOutsideMenu = (event: Event) => {
|
||||
gap: 4px;
|
||||
padding: 3px 0;
|
||||
}
|
||||
.queue-area {
|
||||
order: 2;
|
||||
}
|
||||
.control-pad > button,
|
||||
.clock,
|
||||
.legacy-menu > summary,
|
||||
@@ -779,6 +794,13 @@ const clickOutsideMenu = (event: Event) => {
|
||||
.action-column > div:nth-child(even) {
|
||||
background: #071638;
|
||||
}
|
||||
.action-column > div.autonomous {
|
||||
color: #aaffff;
|
||||
}
|
||||
.action-column small {
|
||||
font-size: 0.72em;
|
||||
line-height: 1;
|
||||
}
|
||||
.edit-column button {
|
||||
background: #444;
|
||||
cursor: pointer;
|
||||
@@ -786,6 +808,7 @@ const clickOutsideMenu = (event: Event) => {
|
||||
.advanced-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 5fr 7fr;
|
||||
order: 1;
|
||||
}
|
||||
.advanced-actions > * {
|
||||
border-radius: 0 !important;
|
||||
@@ -808,6 +831,10 @@ const clickOutsideMenu = (event: Event) => {
|
||||
background: #303030;
|
||||
box-shadow: 0 6px 16px #000;
|
||||
}
|
||||
.reserved-command-editor:not(.compact) .command-picker {
|
||||
max-height: none;
|
||||
overflow: visible;
|
||||
}
|
||||
.command-picker > header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -49,6 +49,7 @@ export type ReservedCommandRow = {
|
||||
time?: string;
|
||||
year?: number;
|
||||
month?: number;
|
||||
autonomous?: boolean;
|
||||
};
|
||||
|
||||
export type CommandPatternEntry = {
|
||||
|
||||
@@ -12,6 +12,7 @@ const props = defineProps<{
|
||||
currentYear?: number;
|
||||
currentMonth?: number;
|
||||
turnTermMinutes?: number;
|
||||
autorunLimit?: number | null;
|
||||
storageKey?: string;
|
||||
}>();
|
||||
|
||||
@@ -43,6 +44,7 @@ const rows = computed<ReservedCommandRow[]>(() => {
|
||||
label: labelMap.value.get(turn.action) ?? turn.action,
|
||||
year: Math.floor(absoluteMonth / 12),
|
||||
month: (absoluteMonth % 12) + 1,
|
||||
autonomous: props.autorunLimit != null && absoluteMonth <= props.autorunLimit - 1,
|
||||
time: date
|
||||
? term >= 5
|
||||
? `${String(date.getUTCHours()).padStart(2, '0')}:${String(date.getUTCMinutes()).padStart(2, '0')}`
|
||||
|
||||
@@ -115,6 +115,33 @@ const isActive = (link: MainNavigationLinkItem) => link.id === 'survey' && props
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.main-global-menu > :deep(.main-menu-link),
|
||||
.main-menu-popup > .main-menu-button,
|
||||
.main-menu-split > :deep(.main-menu-link),
|
||||
.main-menu-split > .main-menu-split__toggle {
|
||||
border-color: var(--sammo-button-navigation-border);
|
||||
background-color: var(--sammo-button-navigation-bg);
|
||||
background-image: none;
|
||||
}
|
||||
|
||||
.main-global-menu > :deep(.main-menu-link:hover),
|
||||
.main-global-menu > :deep(.main-menu-link:focus-visible),
|
||||
.main-global-menu > :deep(.main-menu-link:active),
|
||||
.main-menu-popup > .main-menu-button:hover,
|
||||
.main-menu-popup > .main-menu-button:focus-visible,
|
||||
.main-menu-popup > .main-menu-button:active,
|
||||
.main-menu-popup > .main-menu-button[aria-expanded='true'],
|
||||
.main-menu-split > :deep(.main-menu-link:hover),
|
||||
.main-menu-split > :deep(.main-menu-link:focus-visible),
|
||||
.main-menu-split > :deep(.main-menu-link:active),
|
||||
.main-menu-split > .main-menu-split__toggle:hover,
|
||||
.main-menu-split > .main-menu-split__toggle:focus-visible,
|
||||
.main-menu-split > .main-menu-split__toggle:active,
|
||||
.main-menu-split > .main-menu-split__toggle[aria-expanded='true'] {
|
||||
border-color: var(--sammo-button-navigation-border);
|
||||
background-color: var(--sammo-button-navigation-bg);
|
||||
}
|
||||
|
||||
.main-menu-popup > .main-menu-button,
|
||||
.main-menu-split > :deep(.main-menu-link) {
|
||||
width: 100%;
|
||||
|
||||
@@ -13,6 +13,11 @@ interface MapSummary {
|
||||
year: number;
|
||||
month: number;
|
||||
startYear: number;
|
||||
techLevelLimit?: {
|
||||
maxLevel: number;
|
||||
initialLevel: number;
|
||||
increaseYears: number;
|
||||
};
|
||||
cityList: [number, number, number, number, number, number][];
|
||||
nationList: [number, string, string, number][];
|
||||
myCity?: number | null;
|
||||
@@ -198,9 +203,77 @@ const mapSummary = computed(() => {
|
||||
if (!props.mapData) {
|
||||
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(() => {
|
||||
return `map-theme-${mapTheme.value}`;
|
||||
});
|
||||
@@ -269,6 +342,24 @@ const hoveredCity = computed(() => {
|
||||
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) => {
|
||||
mapStore.setHoveredCity(cityId);
|
||||
};
|
||||
@@ -280,8 +371,13 @@ const selectCity = (cityId: number) => {
|
||||
|
||||
<template>
|
||||
<div class="map-viewer">
|
||||
<div class="map-top">
|
||||
<div class="map-title">{{ mapSummary }}</div>
|
||||
<div class="map-top" :style="titleBandStyle">
|
||||
<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 v-if="props.loading">
|
||||
<SkeletonLines :lines="4" />
|
||||
@@ -309,15 +405,9 @@ const selectCity = (cityId: number) => {
|
||||
@leave="setHoveredCity(null)"
|
||||
@select="selectCity"
|
||||
/>
|
||||
<div
|
||||
v-if="hoveredCity"
|
||||
class="map-tooltip"
|
||||
: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 v-if="hoveredCity" class="map-tooltip" :style="tooltipPosition">
|
||||
<div class="tooltip-title">{{ hoveredCityTitle }}</div>
|
||||
<div class="tooltip-body">{{ hoveredCity.nationId > 0 ? hoveredCity.nationName : '' }}</div>
|
||||
</div>
|
||||
<div class="map-controls">
|
||||
<button class="map-toggle" :class="{ active: showCityName }" @click="mapStore.toggleCityName">
|
||||
@@ -338,17 +428,69 @@ const selectCity = (cityId: number) => {
|
||||
}
|
||||
|
||||
.map-top {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 20px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #111;
|
||||
background-position:
|
||||
left top,
|
||||
right top;
|
||||
background-repeat: no-repeat;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.map-title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
position: relative;
|
||||
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 {
|
||||
@@ -400,19 +542,28 @@ const selectCity = (cityId: number) => {
|
||||
|
||||
.map-tooltip {
|
||||
position: absolute;
|
||||
z-index: 16;
|
||||
box-sizing: border-box;
|
||||
min-width: 120px;
|
||||
pointer-events: none;
|
||||
border: 1px solid rgba(201, 164, 90, 0.4);
|
||||
background: rgba(16, 16, 16, 0.9);
|
||||
padding: 4px 6px;
|
||||
font-size: 0.65rem;
|
||||
border: 1px solid gray;
|
||||
padding: 0;
|
||||
background: rgb(30, 164, 255);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
line-height: 15px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tooltip-title {
|
||||
font-weight: 600;
|
||||
height: 15px;
|
||||
}
|
||||
|
||||
.tooltip-body {
|
||||
color: rgba(232, 221, 196, 0.6);
|
||||
height: 15px;
|
||||
border-top: 1px solid gray;
|
||||
color: #fff;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.map-empty {
|
||||
|
||||
@@ -87,6 +87,13 @@ const bucket = (type: MessageType): MessageEntry[] => props.messages?.[type] ??
|
||||
const visibleMessages = (type: MessageType): MessageEntry[] => bucket(type).slice(0, visibleLimits[type]);
|
||||
|
||||
const permission = computed(() => props.messages?.permission ?? -1);
|
||||
const replyableGeneralIds = computed(() =>
|
||||
props.mailboxGroups.flatMap((group) =>
|
||||
group.options
|
||||
.filter((option) => !option.disabled && option.value > 0 && option.value < 9000)
|
||||
.map((option) => option.value)
|
||||
)
|
||||
);
|
||||
|
||||
const setMailbox = (value: string) => {
|
||||
const parsed = Number(value);
|
||||
@@ -239,6 +246,7 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
||||
:nation-id="nationId"
|
||||
:permission="permission"
|
||||
:can-respond-diplomacy="canRespondDiplomacy"
|
||||
:replyable-general-ids="replyableGeneralIds"
|
||||
@set-target="setReplyTarget"
|
||||
@delete="emit('delete', $event)"
|
||||
@respond="forwardResponse"
|
||||
|
||||
@@ -29,6 +29,7 @@ const props = defineProps<{
|
||||
nationId: number;
|
||||
permission: number;
|
||||
canRespondDiplomacy: boolean;
|
||||
replyableGeneralIds: number[];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -107,6 +108,7 @@ const isBright = (color: string): boolean => {
|
||||
};
|
||||
|
||||
const iconUrl = computed(() => resolveMessageGeneralIconUrl(props.message.src.icon));
|
||||
const canReplyToGeneral = (target: MessageTarget): boolean => props.replyableGeneralIds.includes(target.generalId);
|
||||
|
||||
const targetClass = (target: MessageTarget) => ({
|
||||
'msg-target': true,
|
||||
@@ -167,6 +169,7 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
<span class="msg-from-to">▶</span>
|
||||
<button
|
||||
v-if="canReplyToGeneral(destination)"
|
||||
:class="targetClass(destination)"
|
||||
:style="{ backgroundColor: destination.color }"
|
||||
type="button"
|
||||
@@ -174,9 +177,13 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ destination.generalName }}:{{ destination.nationName }} | ↩
|
||||
</button>
|
||||
<span v-else :class="targetClass(destination)" :style="{ backgroundColor: destination.color }">
|
||||
{{ destination.generalName }}:{{ destination.nationName }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button
|
||||
v-if="canReplyToGeneral(message.src)"
|
||||
:class="targetClass(message.src)"
|
||||
:style="{ backgroundColor: message.src.color }"
|
||||
type="button"
|
||||
@@ -184,6 +191,9 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
||||
</button>
|
||||
<span v-else :class="targetClass(message.src)" :style="{ backgroundColor: message.src.color }">
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }}
|
||||
</span>
|
||||
<span class="msg-from-to">▶</span>
|
||||
<span :class="targetClass(destination)" :style="{ backgroundColor: destination.color }"
|
||||
>나</span
|
||||
@@ -241,7 +251,7 @@ onBeforeUnmount(() => {
|
||||
</template>
|
||||
|
||||
<button
|
||||
v-else-if="message.src.generalId !== generalId"
|
||||
v-else-if="message.src.generalId !== generalId && canReplyToGeneral(message.src)"
|
||||
:class="targetClass(message.src)"
|
||||
:style="{ backgroundColor: message.src.color }"
|
||||
type="button"
|
||||
@@ -249,6 +259,13 @@ onBeforeUnmount(() => {
|
||||
>
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }} | ↩
|
||||
</button>
|
||||
<span
|
||||
v-else-if="message.src.generalId !== generalId"
|
||||
:class="targetClass(message.src)"
|
||||
:style="{ backgroundColor: message.src.color }"
|
||||
>
|
||||
{{ message.src.generalName }}:{{ message.src.nationName }}
|
||||
</span>
|
||||
<span v-else :class="targetClass(message.src)" :style="{ backgroundColor: message.src.color }">
|
||||
{{ message.src.generalName }}
|
||||
</span>
|
||||
|
||||
@@ -65,6 +65,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
boardAccess?: BoardAccess | null;
|
||||
reservedGeneralTurns?: ReservedTurnView[] | null;
|
||||
reservedGeneralRevision?: number;
|
||||
reservedGeneralAutorunLimit?: number | null;
|
||||
globalRecords?: RecentRecord[];
|
||||
generalRecords?: RecentRecord[];
|
||||
worldHistory?: RecentRecord[];
|
||||
@@ -94,6 +95,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
const boardAccess = ref<BoardAccess | null>(null);
|
||||
const reservedGeneralTurns = ref<ReservedTurnView[] | null>(null);
|
||||
const reservedGeneralRevision = ref(0);
|
||||
const reservedGeneralAutorunLimit = ref<number | null>(null);
|
||||
const globalRecords = ref<RecentRecord[]>([]);
|
||||
const generalRecords = ref<RecentRecord[]>([]);
|
||||
const worldHistory = ref<RecentRecord[]>([]);
|
||||
@@ -331,6 +333,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
boardAccess.value = null;
|
||||
reservedGeneralTurns.value = null;
|
||||
reservedGeneralRevision.value = 0;
|
||||
reservedGeneralAutorunLimit.value = null;
|
||||
resetRecentRecords(null);
|
||||
commandTableRevision = null;
|
||||
boardAccessRevision = null;
|
||||
@@ -351,6 +354,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
boardAccess.value = null;
|
||||
reservedGeneralTurns.value = null;
|
||||
reservedGeneralRevision.value = 0;
|
||||
reservedGeneralAutorunLimit.value = null;
|
||||
resetRecentRecords(null);
|
||||
contextRevision = null;
|
||||
commandTableRevision = null;
|
||||
@@ -384,6 +388,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
if (patch.reservedGeneralRevision !== undefined) {
|
||||
reservedGeneralRevision.value = patch.reservedGeneralRevision;
|
||||
}
|
||||
if (patch.reservedGeneralAutorunLimit !== undefined) {
|
||||
reservedGeneralAutorunLimit.value = patch.reservedGeneralAutorunLimit;
|
||||
}
|
||||
if (patch.globalRecords !== undefined) {
|
||||
globalRecords.value = structurallyShare(globalRecords.value, patch.globalRecords);
|
||||
lastGeneralRecordId = Math.max(lastGeneralRecordId, patch.globalRecords[0]?.id ?? 0);
|
||||
@@ -425,6 +432,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
patch.boardAccess = boardAccessSnapshot ?? null;
|
||||
patch.reservedGeneralTurns = toRaw(reservedGeneralTurns.value as unknown) as ReservedTurnView[] | null;
|
||||
patch.reservedGeneralRevision = reservedGeneralRevision.value;
|
||||
patch.reservedGeneralAutorunLimit = reservedGeneralAutorunLimit.value;
|
||||
patch.globalRecords = toRaw(globalRecords.value);
|
||||
patch.generalRecords = toRaw(generalRecords.value);
|
||||
patch.worldHistory = toRaw(worldHistory.value);
|
||||
@@ -552,6 +560,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
generalTurns.turns
|
||||
) as ReservedTurnView[];
|
||||
reservedGeneralRevision.value = generalTurns.revision;
|
||||
reservedGeneralAutorunLimit.value = generalTurns.autorunLimit ?? null;
|
||||
if (records) {
|
||||
applyRecentRecords(records);
|
||||
}
|
||||
@@ -672,6 +681,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
if (generalTurns !== undefined) {
|
||||
patch.reservedGeneralTurns = generalTurns.turns;
|
||||
patch.reservedGeneralRevision = generalTurns.revision;
|
||||
patch.reservedGeneralAutorunLimit = generalTurns.autorunLimit ?? null;
|
||||
}
|
||||
if (records) {
|
||||
const nextGlobalRecords = mergeRecentRecords(globalRecords.value, records.global);
|
||||
@@ -836,12 +846,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
});
|
||||
reservedGeneralTurns.value = result.turns;
|
||||
reservedGeneralRevision.value = result.revision;
|
||||
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
|
||||
} catch (err) {
|
||||
error.value = resolveErrorMessage(err);
|
||||
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
|
||||
if (snapshot) {
|
||||
reservedGeneralTurns.value = snapshot.turns;
|
||||
reservedGeneralRevision.value = snapshot.revision;
|
||||
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -859,12 +871,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
});
|
||||
reservedGeneralTurns.value = result.turns;
|
||||
reservedGeneralRevision.value = result.revision;
|
||||
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
|
||||
} catch (err) {
|
||||
error.value = resolveErrorMessage(err);
|
||||
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
|
||||
if (snapshot) {
|
||||
reservedGeneralTurns.value = snapshot.turns;
|
||||
reservedGeneralRevision.value = snapshot.revision;
|
||||
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -882,12 +896,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
});
|
||||
reservedGeneralTurns.value = result.turns;
|
||||
reservedGeneralRevision.value = result.revision;
|
||||
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
|
||||
} catch (err) {
|
||||
error.value = resolveErrorMessage(err);
|
||||
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
|
||||
if (snapshot) {
|
||||
reservedGeneralTurns.value = snapshot.turns;
|
||||
reservedGeneralRevision.value = snapshot.revision;
|
||||
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -903,12 +919,14 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
});
|
||||
reservedGeneralTurns.value = result.turns;
|
||||
reservedGeneralRevision.value = result.revision;
|
||||
reservedGeneralAutorunLimit.value = result.autorunLimit ?? null;
|
||||
} catch (err) {
|
||||
error.value = resolveErrorMessage(err);
|
||||
const snapshot = await trpc.turns.reserved.getGeneral.query({ generalId: id }).catch(() => null);
|
||||
if (snapshot) {
|
||||
reservedGeneralTurns.value = snapshot.turns;
|
||||
reservedGeneralRevision.value = snapshot.revision;
|
||||
reservedGeneralAutorunLimit.value = snapshot.autorunLimit ?? null;
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1192,6 +1210,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
messageContacts,
|
||||
boardAccess,
|
||||
reservedGeneralTurns,
|
||||
reservedGeneralAutorunLimit,
|
||||
globalRecords,
|
||||
generalRecords,
|
||||
worldHistory,
|
||||
|
||||
@@ -46,6 +46,7 @@ const {
|
||||
messages,
|
||||
boardAccess,
|
||||
reservedGeneralTurns,
|
||||
reservedGeneralAutorunLimit,
|
||||
globalRecords,
|
||||
generalRecords,
|
||||
worldHistory,
|
||||
@@ -218,6 +219,7 @@ watch(
|
||||
:current-year="lobbyInfo?.year"
|
||||
:current-month="lobbyInfo?.month"
|
||||
:turn-term-minutes="lobbyInfo?.turnTerm"
|
||||
:autorun-limit="reservedGeneralAutorunLimit"
|
||||
@set-general-turns="reserveGeneralTurns"
|
||||
@shift-general-turns="shiftGeneralTurns"
|
||||
@repeat-general-turns="repeatGeneralTurns"
|
||||
@@ -341,6 +343,7 @@ watch(
|
||||
:current-year="lobbyInfo?.year"
|
||||
:current-month="lobbyInfo?.month"
|
||||
:turn-term-minutes="lobbyInfo?.turnTerm"
|
||||
:autorun-limit="reservedGeneralAutorunLimit"
|
||||
@set-general-turns="reserveGeneralTurns"
|
||||
@shift-general-turns="shiftGeneralTurns"
|
||||
@repeat-general-turns="repeatGeneralTurns"
|
||||
@@ -571,6 +574,7 @@ button {
|
||||
.layout-desktop {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, minmax(0, 1fr));
|
||||
grid-template-rows: 520px minmax(125px, auto) auto;
|
||||
gap: 0;
|
||||
align-items: start;
|
||||
}
|
||||
@@ -584,32 +588,30 @@ button {
|
||||
|
||||
.layout-desktop > [data-main-target='commands'] {
|
||||
grid-column: 8 / 11;
|
||||
grid-row: 1;
|
||||
height: 645px;
|
||||
grid-row: 1 / 3;
|
||||
min-height: 645px;
|
||||
width: 290px;
|
||||
margin-left: 10px;
|
||||
overflow-y: auto;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.layout-desktop > [data-main-target='city'] {
|
||||
grid-column: 1 / 8;
|
||||
grid-row: 1;
|
||||
grid-row: 2;
|
||||
align-self: stretch;
|
||||
min-height: 125px;
|
||||
margin-top: 520px;
|
||||
}
|
||||
|
||||
.layout-desktop > [data-main-target='nation'] {
|
||||
grid-column: 1 / 6;
|
||||
grid-row: 1;
|
||||
grid-row: 3;
|
||||
min-height: 193px;
|
||||
margin-top: 645px;
|
||||
}
|
||||
|
||||
.layout-desktop > [data-main-target='general'] {
|
||||
grid-column: 6 / 11;
|
||||
grid-row: 1;
|
||||
grid-row: 3;
|
||||
min-height: 193px;
|
||||
margin-top: 645px;
|
||||
}
|
||||
|
||||
.layout-desktop > [data-main-target],
|
||||
@@ -719,8 +721,8 @@ button {
|
||||
}
|
||||
|
||||
.layout-mobile [data-main-target='commands'] {
|
||||
height: 645px;
|
||||
overflow-y: auto;
|
||||
min-height: 645px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.layout-mobile [data-main-target='nation'],
|
||||
|
||||
@@ -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