feat: Vue3로 새롭게 작성한 메인 페이지 (#229)
- v_front.php - API 추가 - General/GetFrontInfo - Global/GetGlobalMenu - DTO 추가 - MenuItem, MenuMulti, MenuSplit - 글로벌 메뉴 출력 방식 변경 - d_setting/GlobalMenu.php Reviewed-on: https://storage.hided.net/gitea/devsam/core/pulls/229
This commit was merged in pull request #229.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<template>
|
||||
<span v-if="autorunMode.limit_minutes > 0" v-b-tooltip.hover :title="tooltipText" style="text-decoration: underline;">자율행동</span>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { AutorunUserMode } from '@/defs/API/Global';
|
||||
import type { Entries } from '@/util/Entries';
|
||||
import { ref, toRef, watch } from 'vue';
|
||||
|
||||
type AutorunMode = {
|
||||
limit_minutes: number,
|
||||
options: Record<AutorunUserMode, number>,
|
||||
};
|
||||
|
||||
const props = defineProps<{
|
||||
autorunMode: AutorunMode
|
||||
}>();
|
||||
|
||||
const tooltipText = ref("_");
|
||||
const autorunMode = toRef(props, 'autorunMode');
|
||||
|
||||
function updateTooltipText(autorunMode: AutorunMode){
|
||||
const {options, limit_minutes} = autorunMode;
|
||||
const optionMap: Record<AutorunUserMode, string> = {
|
||||
'battle': '출병',
|
||||
'warp': '순간이동',
|
||||
'recruit': '징병',
|
||||
'recruit_high': '모병',
|
||||
'train': '훈련/사기진작',
|
||||
'chief': '사령턴',
|
||||
'develop': '내정',
|
||||
};
|
||||
|
||||
const response = new Map<AutorunUserMode | 'limit_minutes', string>();
|
||||
for(const [option, value] of Object.entries(options) as Entries<typeof options>){
|
||||
if(value > 0){
|
||||
response.set(option, optionMap[option]);
|
||||
}
|
||||
}
|
||||
|
||||
if(response.has('recruit_high')){
|
||||
response.delete('recruit');
|
||||
}
|
||||
|
||||
if(limit_minutes >= 43200){
|
||||
response.set('limit_minutes', '항상 유효');
|
||||
}
|
||||
else if(limit_minutes % 60 == 0){
|
||||
response.set('limit_minutes', `${limit_minutes / 60}시간 유효`);
|
||||
}
|
||||
else{
|
||||
response.set('limit_minutes', `${limit_minutes}분 유효`);
|
||||
}
|
||||
|
||||
const text = Array.from(response.values()).join(', ');
|
||||
tooltipText.value = text;
|
||||
}
|
||||
|
||||
updateTooltipText(autorunMode.value);
|
||||
watch(autorunMode, (newAutorunMode) => {
|
||||
updateTooltipText(newAutorunMode);
|
||||
});
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,228 @@
|
||||
<template>
|
||||
<div class="city-card-basic bg2">
|
||||
<div
|
||||
class="cityNamePanel"
|
||||
:style="{
|
||||
color: isBrightColor(city.nationInfo.color) ? 'black' : 'white',
|
||||
backgroundColor: city.nationInfo.color,
|
||||
}"
|
||||
>
|
||||
<div>【{{ cityRegionText }} | {{ cityLevelText }}】 {{ city.name }}</div>
|
||||
</div>
|
||||
<div
|
||||
class="nationNamePanel"
|
||||
:style="{
|
||||
color: isBrightColor(city.nationInfo.color) ? 'black' : 'white',
|
||||
backgroundColor: city.nationInfo.color,
|
||||
}"
|
||||
>
|
||||
{{ city.nationInfo.id ? `지배 국가 【 ${city.nationInfo.name} 】` : "공 백 지" }}
|
||||
</div>
|
||||
<div class="gPanel popPanel">
|
||||
<div class="gHead bg1">주민</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="(city.pop[0] / city.pop[1]) * 100" />
|
||||
<div class="cellText">{{ city.pop[0].toLocaleString() }} / {{ city.pop[1].toLocaleString() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel trustPanel">
|
||||
<div class="gHead bg1">민심</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="city.trust" />
|
||||
<div class="cellText">{{ city.trust.toLocaleString() }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel agriPanel">
|
||||
<div class="gHead bg1">농업</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="(city.agri[0] / city.agri[1]) * 100" />
|
||||
<div class="cellText">
|
||||
{{ city.agri[0].toLocaleString() }}
|
||||
/
|
||||
{{ city.agri[1].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel commPanel">
|
||||
<div class="gHead bg1">상업</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="(city.comm[0] / city.comm[1]) * 100" />
|
||||
<div class="cellText">
|
||||
{{ city.comm[0].toLocaleString() }}
|
||||
/
|
||||
{{ city.comm[1].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel secuPanel">
|
||||
<div class="gHead bg1">치안</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="(city.secu[0] / city.secu[1]) * 100" />
|
||||
<div class="cellText">
|
||||
{{ city.secu[0].toLocaleString() }}
|
||||
/
|
||||
{{ city.secu[1].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel defPanel">
|
||||
<div class="gHead bg1">수비</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="(city.def[0] / city.def[1]) * 100" />
|
||||
<div class="cellText">
|
||||
{{ city.def[0].toLocaleString() }}
|
||||
/
|
||||
{{ city.def[1].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel wallPanel">
|
||||
<div class="gHead bg1">성벽</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="(city.wall[0] / city.wall[1]) * 100" />
|
||||
<div class="cellText">
|
||||
{{ city.wall[0].toLocaleString() }}
|
||||
/
|
||||
{{ city.wall[1].toLocaleString() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel tradePanel">
|
||||
<div class="gHead bg1">시세</div>
|
||||
<div class="gBody">
|
||||
<SammoBar :height="7" :percent="city.trade ?? 100" />
|
||||
<div class="cellText">{{ city.trade ? `${city.trade}%` : "상인 없음" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel officer4Panel">
|
||||
<div class="gHead bg1">태수</div>
|
||||
<div class="gBody cellTextOnly" :style="{ color: getNPCColor(city.officerList[4]?.npc ?? 0) }">
|
||||
{{ city.officerList[4]?.name ?? "-" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel officer3Panel">
|
||||
<div class="gHead bg1">군사</div>
|
||||
<div class="gBody cellTextOnly" :style="{ color: getNPCColor(city.officerList[3]?.npc ?? 0) }">
|
||||
{{ city.officerList[3]?.name ?? "-" }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="gPanel officer2Panel">
|
||||
<div class="gHead bg1">종사</div>
|
||||
<div class="gBody cellTextOnly" :style="{ color: getNPCColor(city.officerList[2]?.npc ?? 0) }">
|
||||
{{ city.officerList[2]?.name ?? "-" }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
import { getNPCColor } from "@/utilGame";
|
||||
import type { GameConstStore } from "@/GameConstStore";
|
||||
import { inject, ref, toRef, watch, type Ref } from "vue";
|
||||
import type { GetFrontInfoResponse } from "@/defs/API/Global";
|
||||
import SammoBar from "@/components/SammoBar.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
city: GetFrontInfoResponse["city"];
|
||||
}>();
|
||||
|
||||
const gameConstStore = unwrap(inject<Ref<GameConstStore>>("gameConstStore"));
|
||||
|
||||
const city = toRef(props, "city");
|
||||
|
||||
const cityRegionText = ref("");
|
||||
const cityLevelText = ref("");
|
||||
watch(
|
||||
city,
|
||||
(city) => {
|
||||
const cityInfo = gameConstStore.value.cityConst[city.id];
|
||||
cityRegionText.value = gameConstStore.value.cityConstMap.region[cityInfo.region] as string;
|
||||
cityLevelText.value = gameConstStore.value.cityConstMap.level[city.level] as string;
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
.city-card-basic {
|
||||
display: grid;
|
||||
border-right: solid 1px gray;
|
||||
border-bottom: solid 1px gray;
|
||||
|
||||
.cellText {
|
||||
text-align: center;
|
||||
line-height: 1.2em;
|
||||
}
|
||||
|
||||
.cellTextOnly {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.gPanel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
border-top: solid 1px gray;
|
||||
border-left: solid 1px gray;
|
||||
|
||||
.gHead {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.cityNamePanel,
|
||||
.nationNamePanel {
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
border-top: solid 1px gray;
|
||||
border-left: solid 1px gray;
|
||||
}
|
||||
|
||||
.popPanel {
|
||||
grid-column: 1 / 3;
|
||||
grid-template-columns: 1fr 5fr;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
.city-card-basic {
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
|
||||
.cityNamePanel,
|
||||
.nationNamePanel {
|
||||
grid-column: 1 / 5;
|
||||
}
|
||||
|
||||
.officer4Panel {
|
||||
grid-column: 4 / 5;
|
||||
grid-row: 3 / 4;
|
||||
}
|
||||
|
||||
.officer3Panel {
|
||||
grid-column: 4 / 5;
|
||||
grid-row: 4 / 5;
|
||||
}
|
||||
|
||||
.officer2Panel {
|
||||
grid-column: 4 / 5;
|
||||
grid-row: 5 / 6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
.city-card-basic {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
|
||||
.cityNamePanel,
|
||||
.nationNamePanel {
|
||||
grid-column: 1 / 4;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,192 @@
|
||||
<template>
|
||||
<nav class="gameBottomBar navbar-expand navbar-dark bg-dark d-sm-block d-md-none p-0">
|
||||
<div class="collapse navbar-collapse">
|
||||
<ul class="navbar-nav me-auto mx-auto">
|
||||
<li class="nav-item dropup">
|
||||
<div
|
||||
id="navbarGlobal"
|
||||
class="dropdown-toggle text-white btn btn-sammo-base2"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
외부 메뉴
|
||||
</div>
|
||||
<GlobalMenuBar
|
||||
id="navbarGlobalItems"
|
||||
aria-labelledby="navbarGlobal"
|
||||
:globalInfo="globalInfo"
|
||||
:modelValue="globalMenu"
|
||||
:columns="3"
|
||||
/>
|
||||
</li>
|
||||
<li class="nav-item dropup">
|
||||
<div
|
||||
id="navbarNation"
|
||||
class="dropdown-toggle btn btn-sammo-nation controlBar"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
국가 메뉴
|
||||
</div>
|
||||
<MainControlDropdown
|
||||
id="navbarNationItems"
|
||||
aria-labelledby="navbarNation"
|
||||
:showSecret="showSecret"
|
||||
:permission="frontInfo.general.permission"
|
||||
:myLevel="frontInfo.general.officerLevel"
|
||||
:nationLevel="nationInfo.level"
|
||||
/>
|
||||
</li>
|
||||
<li class="nav-item dropup">
|
||||
<div
|
||||
id="navbarQuick"
|
||||
class="dropdown-toggle text-white btn btn-dark"
|
||||
role="button"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
빠른 이동
|
||||
</div>
|
||||
<ul id="navbarQuickItems" class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
|
||||
<li><a class="dropdown-item disabled">국가 정보</a></li>
|
||||
<hr class="dropdown-divider" />
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.nationNotice')">방침</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('#reservedCommandPanel')">명령</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.nationInfo')">국가</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.generalInfo')">장수</button>
|
||||
</li>
|
||||
<li><button type="button" class="dropdown-item" @click="scrollToSelector('.cityInfo')">도시</button></li>
|
||||
<li><a class="dropdown-item disabled">동향 정보</a></li>
|
||||
<hr class="dropdown-divider" />
|
||||
<li><button type="button" class="dropdown-item" @click="scrollToSelector('.mapView')">지도</button></li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.PublicRecord')">
|
||||
동향
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.GeneralLog')">개인</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.WorldHistory')">정세</button>
|
||||
</li>
|
||||
<li><span> </span></li>
|
||||
<li><a class="dropdown-item disabled">메시지</a></li>
|
||||
<hr class="dropdown-divider" />
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.PublicTalk > .stickyAnchor')">전체</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.NationalTalk > .stickyAnchor')">국가</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.PrivateTalk > .stickyAnchor')">개인</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item" @click="scrollToSelector('.DiplomacyTalk > .stickyAnchor')">외교</button>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="btn btn-sammo-base2" @click="moveLobby">로비로</button>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="refreshPage btn btn-sammo-base2 text-white" role="button" @click="emit('refresh')">갱신</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GetFrontInfoResponse, GetMenuResponse } from "@/defs/API/Global";
|
||||
import { scrollToSelector } from "@/util/scrollToSelector";
|
||||
import { toRefs, watch, ref, computed } from "vue";
|
||||
import GlobalMenuBar from "./GlobalMenuDropdown.vue";
|
||||
import MainControlDropdown from "./MainControlDropdown.vue";
|
||||
|
||||
//FIXME: scrollToSelector
|
||||
|
||||
const props = defineProps<{
|
||||
frontInfo: GetFrontInfoResponse;
|
||||
globalMenu: GetMenuResponse["menu"];
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "refresh"): void;
|
||||
}>();
|
||||
|
||||
const { frontInfo, globalMenu } = toRefs(props);
|
||||
|
||||
const globalInfo = ref(frontInfo.value.global);
|
||||
watch(frontInfo, (frontInfo) => {
|
||||
globalInfo.value = frontInfo.global;
|
||||
});
|
||||
|
||||
const nationInfo = ref(frontInfo.value.nation);
|
||||
watch(frontInfo, (frontInfo) => {
|
||||
nationInfo.value = frontInfo.nation;
|
||||
});
|
||||
|
||||
function moveLobby() {
|
||||
location.replace("../");
|
||||
}
|
||||
|
||||
const showSecret = computed(() => {
|
||||
if (!frontInfo.value) {
|
||||
return false;
|
||||
}
|
||||
if (frontInfo.value.general.permission >= 1) {
|
||||
return true;
|
||||
}
|
||||
if (frontInfo.value.general.officerLevel >= 2) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
@import "@scss/common/base.scss";
|
||||
|
||||
@include media-500px {
|
||||
.gameBottomBar {
|
||||
.nav-item ul.dropdown-menu {
|
||||
max-height: calc(100vh - 50px);
|
||||
overflow-y: auto;
|
||||
|
||||
li {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item > .btn {
|
||||
text-align: center;
|
||||
width: 125px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
box-shadow: 0 -1px 0 $dark;
|
||||
border-left: none !important;
|
||||
border-right: none !important;
|
||||
}
|
||||
|
||||
#navbarNationItems {
|
||||
columns: 3;
|
||||
}
|
||||
|
||||
#navbarQuickItems {
|
||||
columns: 3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,155 @@
|
||||
<template>
|
||||
<h3 class="scenarioName center">
|
||||
{{ gameConstStore?.gameConst.title }} {{ serverName }}{{ frontInfo?.global.serverCnt }}기
|
||||
<span class="avoid-wrap" style="color: cyan">{{ frontInfo?.global.scenarioText }}</span>
|
||||
</h3>
|
||||
<div v-if="frontInfo" class="gameInfo row gx-0">
|
||||
<div class="s-border-t col py-2 col-8 col-md-4 subScenarioName" style="color: cyan">
|
||||
{{ globalInfo.scenarioText }}
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-4 col-md-2 subNPCType" style="color: cyan">
|
||||
NPC 수, 상성:
|
||||
{{ globalInfo.extendedGeneral ? "확장" : "표준" }}
|
||||
{{ globalInfo.isFiction ? "가상" : "사실" }}
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-4 col-md-2 subNPCMode" style="color: cyan">
|
||||
NPC선택: {{ ["불가능", "가능", "선택 생성"][globalInfo.npcMode] }}
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-4 col-md-2 subTournamentMode" style="color: cyan">
|
||||
토너먼트: 경기당 {{ calcTournamentTerm(globalInfo.turnterm) }}분
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-4 col-md-2 subOtherSetting" style="color: cyan">
|
||||
기타 설정:
|
||||
<AutorunInfo :autorunMode="globalInfo.autorunUser" />
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-8 col-md-4 subYearMonth">
|
||||
현재: {{ globalInfo.year }}年 {{ globalInfo.month }}月 ({{ globalInfo.turnterm }}분 턴 서버)
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-4 col-md-2 subOnlineUserCnt">
|
||||
전체 접속자 수: {{ globalInfo.onlineUserCnt.toLocaleString() }}명
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-4 col-md-2 subAPILimit">
|
||||
턴당 갱신횟수: {{ globalInfo.apiLimit.toLocaleString() }}회
|
||||
</div>
|
||||
<div class="s-border-t col py-2 col-8 col-md-4 subGeneralCnt">
|
||||
등록 장수: 유저
|
||||
{{ createdUserCnt.toLocaleString() }} / {{ globalInfo.generalCntLimit.toLocaleString() }} +
|
||||
<span style="color: cyan">NPC {{ createdNPCCnt.toLocaleString() }} 명</span>
|
||||
</div>
|
||||
<div class="s-border-t py-2 col col-6 col-md-4 subTournamentState">
|
||||
<span v-if="frontInfo.global.tournamentType">
|
||||
<a v-if="tournamentStep.availableJoin" href="b_tournament.php" target="_blank">
|
||||
↑<span style="color: cyan"
|
||||
>{{ formatTournamentType(frontInfo.global.tournamentType) }}
|
||||
<span style="color: orange">{{ tournamentStep.state }}</span> {{ tournamentStep.nextText }}
|
||||
{{ formatTime(tournamentTime).substring(11, 16) }}</span
|
||||
>↑
|
||||
</a>
|
||||
<span v-else>
|
||||
↑<span style="color: cyan"
|
||||
>{{ formatTournamentType(frontInfo.global.tournamentType) }}
|
||||
<span style="color: magenta">{{ tournamentStep.state }}</span> {{ tournamentStep.nextText }}
|
||||
{{ formatTime(tournamentTime).substring(11, 16) }}</span
|
||||
>↑
|
||||
</span>
|
||||
</span>
|
||||
<span v-else style="color: magenta"> 현재 토너먼트 경기 없음 </span>
|
||||
</div>
|
||||
<div
|
||||
class="s-border-t py-2 col col-6 col-md-2 subLastExecuted"
|
||||
:style="{ color: serverLocked ? 'magenta' : 'cyan' }"
|
||||
>
|
||||
동작 시각: {{ formatTime(lastExecuted).substring(5) }}
|
||||
</div>
|
||||
<div class="s-border-t py-2 col col-6 col-md-2 subAuctionState">
|
||||
<a v-if="globalInfo.auctionCount" href="v_auction.php" target="_blank" style="color: cyan">
|
||||
{{ globalInfo.auctionCount.toLocaleString() }}건 거래 진행중
|
||||
</a>
|
||||
<span v-else style="color: magenta">진행중인 거래 없음</span>
|
||||
</div>
|
||||
<div class="s-border-t py-2 col col-6 col-md-4 subVoteState">
|
||||
<a v-if="globalInfo.lastVote" href="v_vote.php" target="_blank">
|
||||
<span style="color: cyan">설문 진행 중: </span><span>{{ globalInfo.lastVote.title }}</span>
|
||||
</a>
|
||||
<span v-else style="color: magenta">진행중인 설문 없음</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { GetFrontInfoResponse } from "@/defs/API/Global";
|
||||
import type { GameConstStore } from "@/GameConstStore";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { inject, toRefs, ref, watch } from "vue";
|
||||
import { formatTime } from "@/util/formatTime";
|
||||
import { calcTournamentTerm } from "@/utilGame";
|
||||
import { formatTournamentStep, type TournamentStepType, formatTournamentType } from "@/utilGame/formatTournament";
|
||||
import { parseTime } from "@/util/parseTime";
|
||||
import AutorunInfo from "./AutorunInfo.vue";
|
||||
|
||||
const props = defineProps<{
|
||||
frontInfo: GetFrontInfoResponse;
|
||||
serverName: string;
|
||||
serverLocked: boolean;
|
||||
lastExecuted: Date;
|
||||
}>();
|
||||
|
||||
const { frontInfo, serverName, serverLocked, lastExecuted } = toRefs(props);
|
||||
|
||||
const globalInfo = ref(frontInfo.value.global);
|
||||
|
||||
const gameConstStore = unwrap(inject<GameConstStore>("gameConstStore"));
|
||||
|
||||
const generalCnt = ref(new Map<number, number>());
|
||||
const createdUserCnt = ref(0);
|
||||
const createdNPCCnt = ref(0);
|
||||
|
||||
const tournamentStep = ref<TournamentStepType>({
|
||||
availableJoin: false,
|
||||
state: "초기화 중",
|
||||
nextText: "",
|
||||
});
|
||||
const tournamentTime = ref<Date>(new Date());
|
||||
|
||||
function updateFrontInfo(frontInfo: GetFrontInfoResponse){
|
||||
const global = frontInfo.global;
|
||||
globalInfo.value = global;
|
||||
|
||||
const value = new Map<number, number>();
|
||||
let userCnt = 0;
|
||||
let npcCnt = 0;
|
||||
for (const [npcType, cnt] of global.genCount) {
|
||||
value.set(npcType, cnt);
|
||||
if (npcType < 2) {
|
||||
userCnt += cnt;
|
||||
} else {
|
||||
npcCnt += cnt;
|
||||
}
|
||||
}
|
||||
generalCnt.value = value;
|
||||
createdUserCnt.value = userCnt;
|
||||
createdNPCCnt.value = npcCnt;
|
||||
|
||||
tournamentStep.value = formatTournamentStep(global.tournamentState);
|
||||
tournamentTime.value = parseTime(global.tournamentTime);
|
||||
}
|
||||
|
||||
watch(frontInfo, updateFrontInfo, {immediate: true});
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
.gameInfo {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subVoteState,
|
||||
.subAuctionState,
|
||||
.subTournamentState {
|
||||
a {
|
||||
text-decoration: gray underline;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,154 @@
|
||||
<template>
|
||||
<div class="global-menu">
|
||||
<template v-for="(item, idx) in filteredMenu" :key="idx">
|
||||
<BButton
|
||||
v-if="item.type === 'item'"
|
||||
class="col"
|
||||
:variant="variant"
|
||||
:href="item.url"
|
||||
:target="item.newTab ? '_blank' : undefined"
|
||||
>{{ item.name }}</BButton
|
||||
>
|
||||
<template v-else-if="item.type === 'multi'">
|
||||
<BDropdown :variant="variant" :text="item.name" class="col">
|
||||
<BDropdownItem
|
||||
v-for="(subItem, subIdx) in item.subMenu"
|
||||
:key="subIdx"
|
||||
:variant="variant"
|
||||
:href="subItem.url"
|
||||
:target="subItem.newTab ? '_blank' : undefined"
|
||||
>{{ subItem.name }}</BDropdownItem
|
||||
>
|
||||
</BDropdown>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'split'">
|
||||
<BDropdown
|
||||
split
|
||||
class="col"
|
||||
:variant="variant"
|
||||
:text="item.main.name"
|
||||
:splitHref="item.main.url"
|
||||
@click="splitClick(item.main)($event)"
|
||||
>
|
||||
<BDropdownItem
|
||||
v-for="(subItem, subIdx) in item.subMenu"
|
||||
:key="subIdx"
|
||||
:href="subItem.url"
|
||||
:target="subItem.newTab ? '_blank' : undefined"
|
||||
>{{ subItem.name }}</BDropdownItem
|
||||
>
|
||||
</BDropdown>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GetFrontInfoResponse, GetMenuResponse, MenuItem, MenuMulti, MenuSplit } from "@/defs/API/Global";
|
||||
import { BButton, BDropdown, BDropdownItem, type ButtonVariant } from "bootstrap-vue-3";
|
||||
import { isArray } from "lodash";
|
||||
import { computed, toRef } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: GetMenuResponse["menu"];
|
||||
globalInfo: GetFrontInfoResponse["global"];
|
||||
variant: ButtonVariant | 'sammo-base2',
|
||||
mobileRowSize?: number,
|
||||
desktopRowSize?: number,
|
||||
}>();
|
||||
|
||||
const mobileRowSize = computed(() => {
|
||||
return props.mobileRowSize || 4;
|
||||
});
|
||||
|
||||
const desktopRowSize = computed(() => {
|
||||
return props.desktopRowSize || 8;
|
||||
});
|
||||
|
||||
const variant = computed(() => {
|
||||
return props.variant as ButtonVariant;
|
||||
});
|
||||
|
||||
const modelValue = toRef(props, "modelValue");
|
||||
const globalInfo = toRef(props, "globalInfo");
|
||||
|
||||
type MenuVariant = MenuItem | MenuSplit | MenuMulti;
|
||||
|
||||
function filterMenu(menu: MenuVariant | MenuVariant[]): MenuVariant | MenuVariant[] | undefined {
|
||||
if (isArray(menu)) {
|
||||
return menu.filter(filterMenu) as MenuVariant[];
|
||||
}
|
||||
|
||||
if (menu.type === "item") {
|
||||
if (!menu.condShowVar) {
|
||||
return menu;
|
||||
}
|
||||
const cond = menu.condShowVar;
|
||||
if (cond in globalInfo.value) {
|
||||
if (cond.startsWith("!")) {
|
||||
if (!globalInfo.value[cond.slice(1) as keyof GetFrontInfoResponse["global"]]) {
|
||||
return menu;
|
||||
}
|
||||
} else if (globalInfo.value[cond as keyof GetFrontInfoResponse["global"]]) {
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (menu.type === "multi") {
|
||||
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||
if (filtered.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (filtered.length === 1) {
|
||||
return filtered[0];
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (menu.type === "split") {
|
||||
const filterMain = filterMenu(menu.main);
|
||||
if (!filterMain) {
|
||||
return undefined;
|
||||
}
|
||||
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||
if (filtered.length === 0) {
|
||||
return filterMain;
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMenu = computed(() => filterMenu(modelValue.value) as GetMenuResponse["menu"]);
|
||||
|
||||
function splitClick(menu: MenuItem) {
|
||||
return (e: Event) => {
|
||||
if (!menu.newTab) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(menu.url);
|
||||
};
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
.global-menu {
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
.global-menu {
|
||||
grid-template-columns: repeat(v-bind(desktopRowSize), 1fr);
|
||||
}
|
||||
}
|
||||
@include media-500px {
|
||||
.global-menu {
|
||||
grid-template-columns: repeat(v-bind(mobileRowSize), 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<ul class="dropdown-menu dropdown-menu-start">
|
||||
<template v-for="(item, idx) in filteredMenu" :key="idx">
|
||||
<li v-if="item.type === 'item'">
|
||||
<a class="dropdown-item" :href="item.url" :target="item.newTab ? '_blank' : undefined">
|
||||
{{ item.name }}
|
||||
</a>
|
||||
</li>
|
||||
<template v-else-if="item.type === 'multi'">
|
||||
<li :style="{ orphans: item.subMenu.length + 1 }">
|
||||
<span class="dropdown-item disabled">{{ item.name }}</span>
|
||||
</li>
|
||||
<li v-for="(subItem, subIdx) in item.subMenu" :key="subIdx">
|
||||
<a class="dropdown-item subItem" :href="subItem.url" :target="subItem.newTab ? '_blank' : undefined">{{
|
||||
subItem.name
|
||||
}}</a>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'split'">
|
||||
<li :style="{ orphans: item.subMenu.length + 1 }">
|
||||
<a class="dropdown-item" :href="item.main.url" :target="item.main.newTab ? '_blank' : undefined">{{
|
||||
item.main.name
|
||||
}}</a>
|
||||
</li>
|
||||
<li v-for="(subItem, subIdx) in item.subMenu" :key="subIdx">
|
||||
<a class="dropdown-item subItem" :href="subItem.url" :target="subItem.newTab ? '_blank' : undefined">{{
|
||||
subItem.name
|
||||
}}</a>
|
||||
</li>
|
||||
</template>
|
||||
</template>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { GetFrontInfoResponse, GetMenuResponse, MenuItem, MenuMulti, MenuSplit } from "@/defs/API/Global";
|
||||
import { isArray } from "lodash";
|
||||
import { computed, toRef } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: GetMenuResponse["menu"];
|
||||
globalInfo: GetFrontInfoResponse["global"];
|
||||
mobileRowSize?: number;
|
||||
desktopRowSize?: number;
|
||||
columns?: number;
|
||||
}>();
|
||||
|
||||
const columns = computed(() => {
|
||||
return props.columns ?? 3;
|
||||
});
|
||||
|
||||
const modelValue = toRef(props, "modelValue");
|
||||
const globalInfo = toRef(props, "globalInfo");
|
||||
|
||||
type MenuVariant = MenuItem | MenuSplit | MenuMulti;
|
||||
|
||||
function filterMenu(menu: MenuVariant | MenuVariant[]): MenuVariant | MenuVariant[] | undefined {
|
||||
if (isArray(menu)) {
|
||||
return menu.filter(filterMenu) as MenuVariant[];
|
||||
}
|
||||
|
||||
if (menu.type === "item") {
|
||||
if (!menu.condShowVar) {
|
||||
return menu;
|
||||
}
|
||||
const cond = menu.condShowVar;
|
||||
if (cond in globalInfo.value) {
|
||||
if (cond.startsWith("!")) {
|
||||
if (!globalInfo.value[cond.slice(1) as keyof GetFrontInfoResponse["global"]]) {
|
||||
return menu;
|
||||
}
|
||||
} else if (globalInfo.value[cond as keyof GetFrontInfoResponse["global"]]) {
|
||||
return menu;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (menu.type === "multi") {
|
||||
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||
if (filtered.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (filtered.length === 1) {
|
||||
return filtered[0];
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
|
||||
if (menu.type === "split") {
|
||||
const filterMain = filterMenu(menu.main);
|
||||
if (!filterMain) {
|
||||
return undefined;
|
||||
}
|
||||
const filtered = menu.subMenu.filter(filterMenu) as MenuVariant[];
|
||||
if (filtered.length === 0) {
|
||||
return filterMain;
|
||||
}
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
||||
const filteredMenu = computed(() => filterMenu(modelValue.value) as GetMenuResponse["menu"]);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
.subItem {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
columns: v-bind(columns);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,116 @@
|
||||
<template>
|
||||
<div class="controlBar">
|
||||
<a href="v_board.php" :class="`commandButton btn btn-sammo-nation ${myLevel >= 1 ? '' : 'disabled'}`">회 의 실</a>
|
||||
<a
|
||||
href="v_board.php?isSecret=true"
|
||||
:class="`commandButton ${permission >= 2 ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||
>기 밀 실</a
|
||||
>
|
||||
<a
|
||||
href="v_troop.php"
|
||||
:class="`commandButton ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||
>부대 편성</a
|
||||
>
|
||||
<a href="t_diplomacy.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`">외 교 부</a>
|
||||
<a href="b_myBossInfo.php" :class="`commandButton ${myLevel >= 1 ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||
>인 사 부</a
|
||||
>
|
||||
<a href="v_nationStratFinan.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||
>내 무 부</a
|
||||
>
|
||||
<a href="v_chiefCenter.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||
>사 령 부</a
|
||||
>
|
||||
<a href="v_NPCControl.php" :class="`commandButton ${showSecret ? '' : 'disabled'} btn btn-sammo-nation`"
|
||||
>NPC 정책</a
|
||||
>
|
||||
<a
|
||||
href="b_genList.php"
|
||||
target="_blank"
|
||||
:class="`open-window commandButton btn btn-sammo-nation ${showSecret ? '' : 'disabled'}`"
|
||||
>암 행 부</a
|
||||
>
|
||||
<a href="b_tournament.php" target="_blank" class="commandButton btn btn-sammo-nation">토 너 먼 트</a>
|
||||
<a href="b_myKingdomInfo.php" :class="`commandButton btn btn-sammo-nation ${myLevel >= 1 ? '' : 'disabled'}`"
|
||||
>세력 정보</a
|
||||
>
|
||||
<a
|
||||
href="b_myCityInfo.php"
|
||||
:class="`commandButton btn btn-sammo-nation ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'}`"
|
||||
>세력 도시</a
|
||||
>
|
||||
<a href="v_nationGeneral.php" :class="`commandButton btn btn-sammo-nation ${myLevel >= 1 ? '' : 'disabled'}`"
|
||||
>세력 장수</a
|
||||
>
|
||||
<a href="v_globalDiplomacy.php" class="commandButton btn btn-sammo-nation">중원 정보</a>
|
||||
<a href="b_currentCity.php" class="commandButton btn btn-sammo-nation">현재 도시</a>
|
||||
<a
|
||||
href="v_battleCenter.php"
|
||||
target="_blank"
|
||||
:class="`open-window commandButton btn btn-sammo-nation ${showSecret ? '' : 'disabled'}`"
|
||||
>감 찰 부</a
|
||||
>
|
||||
<a href="v_inheritPoint.php" class="commandButton btn btn-sammo-nation">유산 관리</a>
|
||||
<a href="b_myPage.php" class="commandButton btn btn-sammo-nation">내 정보&설정</a>
|
||||
<div class="btn-group">
|
||||
<a href="v_auction.php" target="_blank" class="open-window commandButton btn btn-sammo-nation">경 매 장</a>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sammo-nation dropdown-toggle dropdown-toggle-split"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
<span class="visually-hidden">Toggle Dropdown</span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end">
|
||||
<li>
|
||||
<a href="v_auction.php" target="_blank" class="open-window commandButton dropdown-item">금/쌀 경매장</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="v_auction.php?type=unique" target="_blank" class="open-window commandButton dropdown-item"
|
||||
>유니크 경매장</a
|
||||
>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<a href="b_betting.php" target="_blank" class="commandButton btn btn-sammo-nation">베 팅 장</a>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { toRefs } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
showSecret: boolean;
|
||||
permission: number;
|
||||
myLevel: number;
|
||||
nationLevel: number;
|
||||
}>();
|
||||
|
||||
const { showSecret, permission, myLevel, nationLevel } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
@include media-500px {
|
||||
.controlBar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
.controlBar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<ul class="dropdown-menu dromdown-menu-start">
|
||||
<li><a href="v_board.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'}`">회의실</a></li>
|
||||
<li>
|
||||
<a href="v_board.php?isSecret=true" :class="`dropdown-item ${permission >= 2 ? '' : 'disabled'} `">기밀실</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="v_troop.php" :class="`dropdown-item ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'} `">부대 편성</a>
|
||||
</li>
|
||||
<li><a href="t_diplomacy.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">외교부</a></li>
|
||||
<li><a href="b_myBossInfo.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'} `">인사부</a></li>
|
||||
<li><a href="v_nationStratFinan.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">내무부</a></li>
|
||||
<li><a href="v_chiefCenter.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">사령부</a></li>
|
||||
<li><a href="v_NPCControl.php" :class="`dropdown-item ${showSecret ? '' : 'disabled'} `">NPC 정책</a></li>
|
||||
<li>
|
||||
<a href="b_genList.php" target="_blank" :class="`dropdown-item open-window ${showSecret ? '' : 'disabled'}`"
|
||||
>암행부</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="b_tournament.php" class="dropdown-item" target="_blank">토너먼트</a></li>
|
||||
<li><a href="b_myKingdomInfo.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'}`">세력 정보</a></li>
|
||||
<li>
|
||||
<a href="b_myCityInfo.php" :class="`dropdown-item ${myLevel >= 1 && nationLevel >= 1 ? '' : 'disabled'}`"
|
||||
>세력도시</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="v_nationGeneral.php" :class="`dropdown-item ${myLevel >= 1 ? '' : 'disabled'}`">세력 장수</a></li>
|
||||
<li><a href="v_globalDiplomacy.php" class="dropdown-item">중원 정보</a></li>
|
||||
<li><a href="b_currentCity.php" class="dropdown-item">현재 도시</a></li>
|
||||
<li>
|
||||
<a href="v_battleCenter.php" target="_blank" :class="`dropdown-item open-window ${showSecret ? '' : 'disabled'}`"
|
||||
>감찰부</a
|
||||
>
|
||||
</li>
|
||||
<li><a href="v_inheritPoint.php" class="dropdown-item">유산 관리</a></li>
|
||||
<li><a href="b_myPage.php" class="dropdown-item">내 정보&설정</a></li>
|
||||
<li style="orphans: 2">
|
||||
<a href="v_auction.php" target="_blank" class="dropdown-item open-window">금/쌀 경매장</a>
|
||||
</li>
|
||||
<li><a href="v_auction.php?type=unique" target="_blank" class="dropdown-item open-window">유니크 경매장</a></li>
|
||||
<li><a href="b_betting.php" class="dropdown-item" target="_blank">베팅장</a></li>
|
||||
</ul>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, toRefs } from "vue";
|
||||
|
||||
const props = defineProps<{
|
||||
showSecret: boolean;
|
||||
permission: number;
|
||||
myLevel: number;
|
||||
nationLevel: number;
|
||||
columns?: number;
|
||||
}>();
|
||||
|
||||
const columns = computed(() => {
|
||||
return props.columns ?? 4;
|
||||
});
|
||||
const { showSecret, permission, myLevel, nationLevel } = toRefs(props);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.subItem {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
columns: v-bind(columns);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,755 @@
|
||||
<template>
|
||||
<div class="MessagePanel">
|
||||
<div class="MessageInputForm bg0 row gx-0">
|
||||
<div id="mailbox_list-col" class="col-6 col-md-2 d-grid">
|
||||
<BFormSelect v-model="targetMailbox" class="bg-dark text-white">
|
||||
<optgroup
|
||||
v-for="group of mailboxList"
|
||||
:key="group.label"
|
||||
:label="group.label"
|
||||
:style="{
|
||||
backgroundColor: group.color,
|
||||
color: !group.color ? undefined : isBrightColor(group.color) ? '#000000' : '#ffffff',
|
||||
}"
|
||||
>
|
||||
<BFormSelectOption
|
||||
v-for="target of group.options"
|
||||
:key="target.value"
|
||||
:disabled="target.disabled"
|
||||
:value="target.value"
|
||||
:style="{
|
||||
backgroundColor: target.color ?? '#000000',
|
||||
color: isBrightColor(target.color ?? '#000000') ? '#000000' : '#ffffff',
|
||||
}"
|
||||
>{{ target.text }}
|
||||
</BFormSelectOption>
|
||||
</optgroup>
|
||||
</BFormSelect>
|
||||
</div>
|
||||
<div id="msg_input-col" class="col-12 col-md-8 d-grid">
|
||||
<input v-model="newMessageText" type="text" maxlength="99" class="form-control" @keydown.enter="sendMessage" />
|
||||
</div>
|
||||
<div id="msg_submit-col" class="col-6 col-md-2 d-grid">
|
||||
<BButton variant="primary" @click="sendMessage">서신전달&갱신</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="PublicTalk">
|
||||
<div class="stickyAnchor"></div>
|
||||
<div class="BoardHeader bg0">전체 메시지</div>
|
||||
<template v-if="messagePublic.length == 0">
|
||||
<div>메시지가 없습니다.</div>
|
||||
</template>
|
||||
<div v-else class="MessageList">
|
||||
<MessagePlate
|
||||
v-for="msg of messagePublic"
|
||||
:key="msg.id"
|
||||
:modelValue="msg"
|
||||
:generalID="generalID"
|
||||
:generalName="generalName"
|
||||
:nationID="nationID"
|
||||
:permissionLevel="permissionLevel"
|
||||
></MessagePlate>
|
||||
<div class="d-grid Actions">
|
||||
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'public')">접기</button>
|
||||
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'public')">
|
||||
이전 메시지 불러오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="NationalTalk">
|
||||
<div class="stickyAnchor"></div>
|
||||
<div class="BoardHeader bg0">국가 메시지</div>
|
||||
<template v-if="messageNational.length == 0">
|
||||
<div>메시지가 없습니다.</div>
|
||||
</template>
|
||||
<div v-else class="MessageList">
|
||||
<MessagePlate
|
||||
v-for="msg of messageNational"
|
||||
:key="msg.id"
|
||||
:modelValue="msg"
|
||||
:generalID="generalID"
|
||||
:generalName="generalName"
|
||||
:nationID="nationID"
|
||||
:permissionLevel="permissionLevel"
|
||||
></MessagePlate>
|
||||
<div class="d-grid Actions">
|
||||
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'national')">접기</button>
|
||||
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'national')">
|
||||
이전 메시지 불러오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="PrivateTalk">
|
||||
<div class="stickyAnchor"></div>
|
||||
<div class="BoardHeader bg0 d-flex">
|
||||
<div class="flex-grow-1 align-self-center">개인 메시지</div>
|
||||
<div>
|
||||
<BButton
|
||||
v-if="messagePrivate.length > 0 && latestPrivateMsgToastInfo[2] > latestPrivateMsgToastInfo[1]"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
@click="readLatestMsg('private')"
|
||||
>모두 읽음</BButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="messagePrivate.length == 0">
|
||||
<div>메시지가 없습니다.</div>
|
||||
</template>
|
||||
<div v-else class="MessageList">
|
||||
<MessagePlate
|
||||
v-for="msg of messagePrivate"
|
||||
:key="msg.id"
|
||||
:modelValue="msg"
|
||||
:generalID="generalID"
|
||||
:generalName="generalName"
|
||||
:nationID="nationID"
|
||||
:permissionLevel="permissionLevel"
|
||||
></MessagePlate>
|
||||
<div class="d-grid Actions">
|
||||
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'private')">접기</button>
|
||||
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'private')">
|
||||
이전 메시지 불러오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="DiplomacyTalk">
|
||||
<div class="stickyAnchor"></div>
|
||||
<div class="BoardHeader bg0 d-flex">
|
||||
<div class="flex-grow-1 align-self-center">외교 메시지</div>
|
||||
<div>
|
||||
<BButton
|
||||
v-if="messageDiplomacy.length > 0 && latestDiplomacyMsgToastInfo[2] > latestDiplomacyMsgToastInfo[1]"
|
||||
ize="sm"
|
||||
variant="secondary"
|
||||
@click="readLatestMsg('diplomacy')"
|
||||
>모두 읽음</BButton
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="messageDiplomacy.length == 0">
|
||||
<div>메시지가 없습니다.</div>
|
||||
</template>
|
||||
<div v-else class="MessageList">
|
||||
<MessagePlate
|
||||
v-for="msg of messageDiplomacy"
|
||||
:key="msg.id"
|
||||
:modelValue="msg"
|
||||
:generalID="generalID"
|
||||
:generalName="generalName"
|
||||
:nationID="nationID"
|
||||
:permissionLevel="permissionLevel"
|
||||
></MessagePlate>
|
||||
<div class="d-grid Actions">
|
||||
<button type="button" class="btn btn-dark only-mobile" @click="foldMessage($event, 'diplomacy')">접기</button>
|
||||
<button type="button" class="btn btn-secondary" @click="loadOldMessage($event, 'diplomacy')">
|
||||
이전 메시지 불러오기
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
declare const staticValues: {
|
||||
serverName: string;
|
||||
serverNick: string;
|
||||
serverID: string;
|
||||
mapName: string;
|
||||
unitSet: string;
|
||||
};
|
||||
</script>
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref, toRef, watch, type Ref } from "vue";
|
||||
import { delay } from "@/util/delay";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
import { isString } from "lodash";
|
||||
import type { MabilboxListResponse, MsgItem, MsgResponse, MsgType } from "@/defs/API/Message";
|
||||
import MessagePlate from "@/components/MessagePlate.vue";
|
||||
import { useToast, BFormSelect } from "bootstrap-vue-3";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
|
||||
const serverID = staticValues.serverID;
|
||||
const toasts = unwrap(useToast());
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "request-refresh"): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<{
|
||||
generalID: number;
|
||||
generalName: string;
|
||||
nationID: number;
|
||||
permissionLevel: number;
|
||||
}>();
|
||||
|
||||
const generalID = toRef(props, "generalID");
|
||||
const generalName = toRef(props, "generalName");
|
||||
const nationID = toRef(props, "nationID");
|
||||
const permissionLevel = toRef(props, "permissionLevel");
|
||||
|
||||
let nationMailbox = nationID.value + 9000;
|
||||
watch(nationID, (newVal) => {
|
||||
nationMailbox = newVal + 9000;
|
||||
});
|
||||
|
||||
const lastSequence = ref(-1);
|
||||
|
||||
const initRefreshLimit = 20;
|
||||
let refreshLimit = initRefreshLimit;
|
||||
const refreshP: Set<Promise<boolean>> = new Set();
|
||||
let lastRefreshDone = 0;
|
||||
let refreshTimer: null | number = null;
|
||||
|
||||
const messageStorage = new Map<number, MsgItem>();
|
||||
const messagePublic = ref<MsgItem[]>([]);
|
||||
const messageNational = ref<MsgItem[]>([]);
|
||||
const messagePrivate = ref<MsgItem[]>([]);
|
||||
const messageDiplomacy = ref<MsgItem[]>([]);
|
||||
|
||||
const messageIndexedList: Record<MsgType, Ref<MsgItem[]>> = {
|
||||
public: messagePublic,
|
||||
national: messageNational,
|
||||
private: messagePrivate,
|
||||
diplomacy: messageDiplomacy,
|
||||
};
|
||||
|
||||
function generateLatestMsgState(msgType: MsgType) {
|
||||
const storageKey = `state.${serverID}.latestReadMsg.${msgType}`;
|
||||
const latestReadMsgID = parseInt(localStorage.getItem(storageKey) ?? "0");
|
||||
const obj = ref<[string | undefined, number, number]>([undefined, latestReadMsgID, latestReadMsgID]);
|
||||
watch(obj, ([, newMsgID], [, oldMsgID]) => {
|
||||
if (newMsgID == oldMsgID) {
|
||||
return;
|
||||
}
|
||||
localStorage.setItem(storageKey, newMsgID.toString());
|
||||
});
|
||||
return obj;
|
||||
}
|
||||
|
||||
const latestPrivateMsgToastInfo = generateLatestMsgState("private");
|
||||
const latestDiplomacyMsgToastInfo = generateLatestMsgState("diplomacy");
|
||||
|
||||
function readLatestMsg(msgType: MsgType) {
|
||||
const targetMap = {
|
||||
private: latestPrivateMsgToastInfo,
|
||||
diplomacy: latestDiplomacyMsgToastInfo,
|
||||
public: undefined,
|
||||
national: undefined,
|
||||
};
|
||||
|
||||
const target = targetMap[msgType];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [toastID, , lastestReceivedID] = target.value;
|
||||
if (toastID) {
|
||||
toasts.remove(toastID);
|
||||
}
|
||||
target.value = [undefined, lastestReceivedID, lastestReceivedID];
|
||||
}
|
||||
|
||||
function _updateLatestMsg(msg: MsgItem) {
|
||||
const msgType = msg.msgType;
|
||||
|
||||
//TODO: 메시지함으로 바로 이동하는 기능이 나중에 필요할 것
|
||||
if (msgType == "private") {
|
||||
const [toastID, latestMsgID] = latestPrivateMsgToastInfo.value;
|
||||
if (msg.id <= latestMsgID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toastID) {
|
||||
toasts.remove(toastID);
|
||||
}
|
||||
const newToastID = toasts.show(
|
||||
{
|
||||
title: "새로운 개인 메시지",
|
||||
body: "새로운 개인 메시지가 도착했습니다.",
|
||||
},
|
||||
{
|
||||
delay: 1000 * 60 * 10,
|
||||
variant: 'warning'
|
||||
}
|
||||
).options.id;
|
||||
latestPrivateMsgToastInfo.value = [newToastID, latestMsgID, msg.id];
|
||||
return;
|
||||
}
|
||||
|
||||
if (msgType == "diplomacy") {
|
||||
const [toastID, latestMsgID] = latestDiplomacyMsgToastInfo.value;
|
||||
if (msg.id <= latestMsgID) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (toastID) {
|
||||
toasts.remove(toastID);
|
||||
}
|
||||
const newToastID = toasts.show(
|
||||
{
|
||||
title: "새로운 외교 메시지",
|
||||
body: "새로운 외교 메시지가 도착했습니다.",
|
||||
},
|
||||
{
|
||||
delay: 1000 * 60 * 10,
|
||||
variant: 'warning'
|
||||
}
|
||||
).options.id;
|
||||
latestDiplomacyMsgToastInfo.value = [newToastID, latestMsgID, msg.id];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function updateLatestMsg(msg: MsgItem[]) {
|
||||
for (const msgItem of msg) {
|
||||
if (msgItem.src.id == generalID.value) continue;
|
||||
_updateLatestMsg(msgItem);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function processMsg(msg: MsgItem) {
|
||||
if (msg.option.delete) {
|
||||
(() => {
|
||||
const targetID = msg.option.delete;
|
||||
const targetMsg = messageStorage.get(targetID);
|
||||
if (!targetMsg) {
|
||||
return;
|
||||
}
|
||||
targetMsg.option.invalid = true;
|
||||
})();
|
||||
}
|
||||
}
|
||||
|
||||
function updateMsgResponse(response: MsgResponse) {
|
||||
if (!response.keepRecent) {
|
||||
messageStorage.clear();
|
||||
for (const msgList of Object.values(messageIndexedList)) {
|
||||
msgList.value.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (response.generalName != generalName.value) {
|
||||
emit("request-refresh");
|
||||
return;
|
||||
}
|
||||
if (response.nationID != nationID.value) {
|
||||
emit("request-refresh");
|
||||
return;
|
||||
}
|
||||
|
||||
lastSequence.value = Math.max(lastSequence.value, response.sequence);
|
||||
|
||||
for (const msgType of Object.keys(messageIndexedList) as (keyof typeof messageIndexedList)[]) {
|
||||
const msgList = messageIndexedList[msgType];
|
||||
|
||||
const newMsgList = response[msgType];
|
||||
if (newMsgList.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
//순서가 어떤 순서인지 모르니, 여기서 내림차순으로 맞춘다.
|
||||
newMsgList.sort((a, b) => b.id - a.id);
|
||||
|
||||
if (msgList.value.length == 0) {
|
||||
msgList.value = newMsgList;
|
||||
updateLatestMsg(newMsgList);
|
||||
continue;
|
||||
}
|
||||
|
||||
//head test
|
||||
const filteredMsgList: MsgItem[] = [];
|
||||
|
||||
for (const msg of newMsgList) {
|
||||
const oldMsg = messageStorage.get(msg.id);
|
||||
if (oldMsg !== undefined) {
|
||||
continue;
|
||||
}
|
||||
processMsg(msg);
|
||||
messageStorage.set(msg.id, msg);
|
||||
filteredMsgList.push(msg);
|
||||
}
|
||||
|
||||
if (filteredMsgList.length == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const msgHeadID = msgList.value[0].id;
|
||||
const newMsgTailID = filteredMsgList[filteredMsgList.length - 1].id;
|
||||
if (newMsgTailID > msgHeadID) {
|
||||
msgList.value = [...filteredMsgList, ...msgList.value];
|
||||
updateLatestMsg(filteredMsgList);
|
||||
continue;
|
||||
}
|
||||
|
||||
const msgTailID = msgList.value[msgList.value.length - 1].id;
|
||||
const newMsgHeadID = filteredMsgList[0].id;
|
||||
|
||||
if (msgTailID > newMsgHeadID) {
|
||||
msgList.value.push(...filteredMsgList);
|
||||
continue;
|
||||
}
|
||||
|
||||
//중간에 삽입되는 경우는 에러이다.
|
||||
console.error("중간 삽입 있음", msgType, newMsgList);
|
||||
}
|
||||
}
|
||||
|
||||
function beginRefreshTimer() {
|
||||
if (refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
}
|
||||
refreshTimer = window.setInterval(function () {
|
||||
const now = Date.now();
|
||||
if (lastRefreshDone + 5000 < now) {
|
||||
//만약 서버 응답이 없다면?
|
||||
|
||||
if (refreshP.size > refreshLimit && refreshTimer) {
|
||||
clearInterval(refreshTimer);
|
||||
refreshTimer = null;
|
||||
toasts.danger(
|
||||
{
|
||||
title: "메시지 자동 갱신 실패",
|
||||
body: "서버 응답이 없습니다. 새로고침을 해주세요.",
|
||||
},
|
||||
{
|
||||
delay: 1000 * 3600,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
void tryRefresh();
|
||||
}
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
async function tryRefresh() {
|
||||
if (refreshP.size > 0) {
|
||||
await Promise.race([...refreshP, delay(500)]);
|
||||
}
|
||||
|
||||
const waiterP = (async () => {
|
||||
let response: MsgResponse | undefined = undefined;
|
||||
try {
|
||||
response = await SammoAPI.Message.GetRecentMessage({
|
||||
sequence: lastSequence.value,
|
||||
});
|
||||
|
||||
const now = Date.now();
|
||||
if (lastRefreshDone < now) {
|
||||
lastRefreshDone = now;
|
||||
}
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.warning({
|
||||
title: "갱신 실패",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return false;
|
||||
}
|
||||
|
||||
updateMsgResponse(response);
|
||||
|
||||
return true;
|
||||
})();
|
||||
void waiterP.then((result) => {
|
||||
if (!result) {
|
||||
console.error("?! result");
|
||||
refreshLimit--;
|
||||
} else {
|
||||
refreshLimit = initRefreshLimit;
|
||||
}
|
||||
refreshP.delete(waiterP);
|
||||
});
|
||||
refreshP.add(waiterP);
|
||||
|
||||
return waiterP;
|
||||
}
|
||||
|
||||
const targetMailbox = ref(nationID.value + 9000);
|
||||
type MailboxGroup = {
|
||||
label: string;
|
||||
color?: string;
|
||||
options: MailboxTarget[];
|
||||
};
|
||||
type MailboxTarget = {
|
||||
value: number;
|
||||
text: string;
|
||||
nationID: number;
|
||||
disabled?: true;
|
||||
color?: string;
|
||||
};
|
||||
const mailboxList = ref<MailboxGroup[]>([]);
|
||||
const newMessageText = ref<string>("");
|
||||
|
||||
function refreshMailboxList(obj: MabilboxListResponse) {
|
||||
let myNationColor = "#000000";
|
||||
const diplomacyMailboxList: MailboxGroup = {
|
||||
label: "외교메시지",
|
||||
color: "#000000",
|
||||
options: [],
|
||||
};
|
||||
const nationMailboxList: MailboxGroup[] = [];
|
||||
obj.nation.sort(function (lhs, rhs) {
|
||||
if (lhs.mailbox == nationMailbox) {
|
||||
return -1;
|
||||
}
|
||||
if (rhs.mailbox == nationMailbox) {
|
||||
return 1;
|
||||
}
|
||||
return lhs.mailbox - rhs.mailbox;
|
||||
});
|
||||
|
||||
for (const nation of obj.nation) {
|
||||
if (nationMailbox == nation.mailbox) {
|
||||
myNationColor = nation.color;
|
||||
//nationColor저장하는 코드가 있었음
|
||||
}
|
||||
|
||||
const nationBox: MailboxGroup = {
|
||||
label: nation.name,
|
||||
color: nation.color,
|
||||
options: [],
|
||||
};
|
||||
|
||||
nation.general.sort(function (lhs, rhs) {
|
||||
if (lhs[1] < rhs[1]) {
|
||||
return -1;
|
||||
}
|
||||
if (lhs[1] > rhs[1]) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
for (const [destGeneralID, destGeneralName, destGeneralFlag] of nation.general) {
|
||||
const isRuler = !!(destGeneralFlag & 0x1);
|
||||
const isAmbassador = !!(destGeneralFlag & 0x4);
|
||||
|
||||
if (destGeneralID == generalID.value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let textName = destGeneralName;
|
||||
if (isRuler) {
|
||||
textName = `*${textName}*`;
|
||||
} else if (isAmbassador) {
|
||||
textName = `#${textName}#`;
|
||||
}
|
||||
|
||||
const target: MailboxTarget = {
|
||||
value: destGeneralID,
|
||||
text: textName,
|
||||
nationID: nation.nationID,
|
||||
};
|
||||
|
||||
if (permissionLevel.value == 4 && isAmbassador && nationMailbox != nation.mailbox) {
|
||||
target.disabled = true;
|
||||
}
|
||||
nationBox.options.push(target);
|
||||
}
|
||||
nationMailboxList.push(nationBox);
|
||||
|
||||
if (permissionLevel.value < 4 || nationMailbox == nation.mailbox) {
|
||||
continue;
|
||||
}
|
||||
|
||||
diplomacyMailboxList.options.push({
|
||||
value: nation.mailbox,
|
||||
text: nation.name,
|
||||
nationID: nation.nationID,
|
||||
color: nation.color,
|
||||
});
|
||||
}
|
||||
|
||||
const favoriteBox: MailboxGroup = {
|
||||
label: "즐겨찾기",
|
||||
color: "#000000",
|
||||
options: [
|
||||
{
|
||||
value: nationMailbox,
|
||||
text: "【 아국 메세지 】",
|
||||
nationID: nationID.value,
|
||||
color: myNationColor,
|
||||
},
|
||||
{
|
||||
value: 9999,
|
||||
text: "【 전체 메세지 】",
|
||||
nationID: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
mailboxList.value = [favoriteBox, diplomacyMailboxList, ...nationMailboxList];
|
||||
}
|
||||
|
||||
function foldMessage($event: MouseEvent, type: MsgType) {
|
||||
const target = messageIndexedList[type].value;
|
||||
if (target.length < 10) {
|
||||
return;
|
||||
}
|
||||
const remain = target.slice(10);
|
||||
target.length = 10;
|
||||
|
||||
for (const msg of remain) {
|
||||
messageStorage.delete(msg.id);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadOldMessage($event: MouseEvent, type: MsgType) {
|
||||
const target = messageIndexedList[type].value;
|
||||
if (target.length == 0) {
|
||||
return;
|
||||
}
|
||||
const last = target[target.length - 1].id;
|
||||
|
||||
try {
|
||||
const response = await SammoAPI.Message.GetOldMessage({
|
||||
to: last,
|
||||
type,
|
||||
});
|
||||
updateMsgResponse(response);
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.warning({
|
||||
title: "이전 메시지 불러오기 실패",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessage() {
|
||||
const text = newMessageText.value;
|
||||
if (!text) {
|
||||
return tryRefresh();
|
||||
}
|
||||
const mailbox = targetMailbox.value;
|
||||
|
||||
try {
|
||||
const waiter = SammoAPI.Message.SendMessage({
|
||||
mailbox,
|
||||
text,
|
||||
});
|
||||
newMessageText.value = "";
|
||||
await waiter;
|
||||
await tryRefresh();
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.warning({
|
||||
title: "메시지 전송 실패",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async function tryFullRefresh() {
|
||||
try {
|
||||
const refreshP = tryRefresh();
|
||||
const response = await SammoAPI.Message.GetContactList();
|
||||
refreshMailboxList(response);
|
||||
await refreshP;
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
toasts.warning({
|
||||
title: " 실패했습니다.",
|
||||
body: e,
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
|
||||
beginRefreshTimer();
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
tryRefresh,
|
||||
tryFullRefresh,
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
await tryFullRefresh();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@import "@scss/common/break_500px.scss";
|
||||
|
||||
.BoardHeader {
|
||||
color: white;
|
||||
outline-style: solid;
|
||||
outline-width: 1px;
|
||||
outline-color: gray;
|
||||
}
|
||||
|
||||
@include media-1000px {
|
||||
.MessagePanel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.MessageInputForm {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.PublicTalk {
|
||||
border-right: 1px solid gray;
|
||||
}
|
||||
|
||||
.PrivateTalk {
|
||||
border-right: 1px solid gray;
|
||||
}
|
||||
|
||||
.only-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.MessageList {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
height: 650px;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-500px {
|
||||
#msg_submit-col {
|
||||
order: 2;
|
||||
}
|
||||
#msg_input-col {
|
||||
order: 3;
|
||||
}
|
||||
|
||||
.MessageInputForm {
|
||||
position: sticky;
|
||||
top: 0px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.stickyAnchor {
|
||||
position: relative;
|
||||
top: -68px;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.d-grid.Actions {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,360 @@
|
||||
<template>
|
||||
<div
|
||||
:id="`msg_${msg.id}`"
|
||||
:class="['msg_plate', `msg_plate_${msg.msgType}`, `msg_plate_${nationType}`]"
|
||||
:data-id="msg.id"
|
||||
>
|
||||
<div class="msg_icon">
|
||||
<img v-if="src.icon" class="generalIcon" width="64" height="64" :src="encodeURI(src.icon)" />
|
||||
<img v-else class="generalIcon" width="64" height="64" :src="encodeURI(defaultIcon)" />
|
||||
</div>
|
||||
<div class="msg_body">
|
||||
<div class="msg_header">
|
||||
<template v-if="deletable">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn btn-outline-warning btn-sm btn-delete-msg"
|
||||
style="float: right"
|
||||
@click="tryDelete"
|
||||
>
|
||||
❌
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template v-if="msg.msgType == 'private'">
|
||||
<template v-if="src.name == generalName">
|
||||
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }">나</span
|
||||
><span class="msg_from_to">▶</span
|
||||
><span :class="`msg_target msg_${destColorType}`" :style="{ backgroundColor: dest.color }"
|
||||
>{{ dest.name }}:{{ dest.nation }}</span
|
||||
>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }"
|
||||
>{{ src.name }}:{{ src.nation }}</span
|
||||
><span class="msg_from_to">▶</span
|
||||
><span :class="`msg_target msg_${destColorType}`" :style="{ backgroundColor: dest.color }">나</span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else-if="msg.msgType == 'national' && src.nation_id === dest.nation_id">
|
||||
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }">{{ src.name }}</span>
|
||||
</template>
|
||||
<template v-else-if="msg.msgType == 'national' || msg.msgType == 'diplomacy'">
|
||||
<template v-if="src.nation_id == nationID">
|
||||
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }">{{ src.name }}</span
|
||||
><span class="msg_from_to">▶</span
|
||||
><span :class="`msg_target msg_${destColorType}`" :style="{ backgroundColor: dest.color }">{{
|
||||
dest.nation
|
||||
}}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: src.color }"
|
||||
>{{ src.name }}:{{ src.nation }}</span
|
||||
><span class="msg_from_to"></span>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span :class="`msg_target msg_${srcColorType}`" :style="{ backgroundColor: dest.color }"
|
||||
>{{ src.name }}:{{ src.nation }}</span
|
||||
>
|
||||
</template>
|
||||
<span class="msg_time"><{{ msg.time }}></span>
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html vue/max-attributes-per-line -->
|
||||
<div :class="['msg_content', isValidMsg ? 'msg_valid' : 'msg_invalid']" v-html="isValidMsg ? linkifyStr(msg.text) : '삭제된 메시지입니다'"
|
||||
></div>
|
||||
<div v-if="msg.option.action" class="msg_prompt">
|
||||
<button
|
||||
type="button"
|
||||
class="prompt_yes btn_prompt"
|
||||
:disabled="allowButton ? undefined : true"
|
||||
@click="tryAccept"
|
||||
>
|
||||
수락
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="prompt_no btn_prompt"
|
||||
:disabled="allowButton ? undefined : true"
|
||||
@click="tryDecline"
|
||||
>
|
||||
거절
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { MsgItem, MsgTarget } from "@/defs/API/Message";
|
||||
import { parseTime } from "@/util/parseTime";
|
||||
import { differenceInMilliseconds, addMinutes } from "date-fns/esm";
|
||||
import { computed, ref, toRef, watch, type ComputedRef, type Ref } from "vue";
|
||||
import linkifyStr from "linkifyjs/string";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
import { isError, isString } from "lodash";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
|
||||
const props = defineProps<{
|
||||
modelValue: MsgItem;
|
||||
generalID: number;
|
||||
generalName: string;
|
||||
nationID: number;
|
||||
permissionLevel: number;
|
||||
deleted?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "request-refresh"): void;
|
||||
}>();
|
||||
|
||||
const src: Ref<MsgTarget> = ref(props.modelValue.src);
|
||||
const dest: Ref<MsgTarget> = ref(props.modelValue.dest ?? props.modelValue.src);
|
||||
const srcColorType = computed(() => isBrightColor(src.value.color) ? "bright" : "dark");
|
||||
const destColorType = computed(() => isBrightColor(dest.value.color) ? "bright" : "dark");
|
||||
|
||||
const msg = toRef(props, "modelValue");
|
||||
const defaultIcon = `${window.pathConfig.sharedIcon}/default.jpg`;
|
||||
|
||||
const isValidMsg = ref(true);
|
||||
const deletable = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.deleted,
|
||||
() => {
|
||||
isValidMsg.value = testValidMsg(msg.value);
|
||||
deletable.value = testDeletable(msg.value);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
msg,
|
||||
(msg) => {
|
||||
isValidMsg.value = testValidMsg(msg);
|
||||
deletable.value = testDeletable(msg);
|
||||
|
||||
src.value = msg.src;
|
||||
dest.value = msg.dest ?? {
|
||||
id: 0,
|
||||
name: "",
|
||||
nation: "재야",
|
||||
nation_id: 0,
|
||||
color: "#000000",
|
||||
icon: defaultIcon,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const deletableTimer: Ref<number | undefined> = ref();
|
||||
const allowButton = computed(() => {
|
||||
if (msg.value.msgType != "diplomacy") {
|
||||
return true;
|
||||
}
|
||||
if (props.permissionLevel >= 4) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
function testDeletable(msg: MsgItem): boolean {
|
||||
if (deletableTimer.value) {
|
||||
clearTimeout(deletableTimer.value);
|
||||
}
|
||||
|
||||
if (props.deleted) return false;
|
||||
if (msg.option.action) return false;
|
||||
if (msg.src.id != props.generalID) return false;
|
||||
if (msg.option.invalid) return false;
|
||||
if (!msg.option.deletable) return false;
|
||||
|
||||
const now = new Date();
|
||||
const last5min = addMinutes(parseTime(msg.time), 5);
|
||||
|
||||
const timeDiff = differenceInMilliseconds(last5min, now);
|
||||
|
||||
if (timeDiff <= 0) return false;
|
||||
|
||||
deletableTimer.value = window.setTimeout(() => {
|
||||
deletable.value = testDeletable(msg);
|
||||
}, timeDiff);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
const nationType: ComputedRef<"local" | "src" | "dest"> = computed(() => {
|
||||
if (msg.value.src.nation_id === msg.value.dest?.nation_id) {
|
||||
return "local";
|
||||
}
|
||||
|
||||
if (msg.value.src.nation_id === props.nationID) {
|
||||
return "src";
|
||||
}
|
||||
return "dest";
|
||||
});
|
||||
|
||||
function testValidMsg(msg: MsgItem): boolean {
|
||||
if (props.deleted) {
|
||||
return false;
|
||||
}
|
||||
if (msg.option.invalid) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async function tryDelete() {
|
||||
if (!confirm("삭제하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await SammoAPI.Message.DeleteMessage({ msgID: msg.value.id });
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
}
|
||||
if (isError(e)) {
|
||||
alert(e.message);
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
emit("request-refresh");
|
||||
}
|
||||
|
||||
async function tryAccept() {
|
||||
if (!confirm("수락하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await SammoAPI.Message.DecideMessageResponse({ msgID: msg.value.id, response: true });
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
}
|
||||
if (isError(e)) {
|
||||
alert(e.message);
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
emit("request-refresh");
|
||||
}
|
||||
|
||||
async function tryDecline() {
|
||||
if (!confirm("거절하시겠습니까?")) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await SammoAPI.Message.DecideMessageResponse({ msgID: msg.value.id, response: false });
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
}
|
||||
if (isError(e)) {
|
||||
alert(e.message);
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
emit("request-refresh");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.msg_plate {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: 64px 1fr;
|
||||
border-bottom: solid 1px gray;
|
||||
min-height: 64px;
|
||||
font-size: 12.5px;
|
||||
word-break: break-all;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.msg_plate_private {
|
||||
background-color: #5d1e1a;
|
||||
}
|
||||
|
||||
.msg_plate_private.msg_plate_dest {
|
||||
background-color: #5d461a;
|
||||
}
|
||||
|
||||
.msg_plate_public {
|
||||
background-color: #141c65;
|
||||
}
|
||||
|
||||
.msg_plate_national,
|
||||
.msg_plate_diplomacy {
|
||||
background-color: #00582c;
|
||||
}
|
||||
|
||||
.msg_plate_national.msg_plate_dest,
|
||||
.msg_plate_diplomacy.msg_plate_dest {
|
||||
background-color: #704615;
|
||||
}
|
||||
|
||||
.msg_plate_national.msg_plate_src,
|
||||
.msg_plate_diplomacy.msg_plate_src {
|
||||
background-color: #70153b;
|
||||
}
|
||||
|
||||
.msg_icon {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-right: solid 1px gray;
|
||||
}
|
||||
|
||||
.msg_time {
|
||||
font-size: 0.75em;
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.msg_header {
|
||||
font-weight: bold;
|
||||
margin-bottom: 3px;
|
||||
color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.msg_invalid {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.msg_content {
|
||||
margin-left: 10px;
|
||||
margin-right: 5px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.msg_target {
|
||||
margin: 2px 2px 0 2px;
|
||||
padding: 2px 3px;
|
||||
display: inline-block;
|
||||
box-shadow: 2px 2px black;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.msg_target.msg_bright {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.msg_target.msg_dark {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.msg_from_to {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.msg_prompt {
|
||||
text-align: right;
|
||||
margin-top: 5px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.btn-delete-msg {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
margin: 2px 2px 0 2px;
|
||||
font-size: 8px;
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -0,0 +1,198 @@
|
||||
<template>
|
||||
<div class="nation-card-basic bg2">
|
||||
<div
|
||||
class="name tb-title"
|
||||
:style="{
|
||||
backgroundColor: nation.color,
|
||||
color: isBrightColor(nation.color) ? 'black' : 'white',
|
||||
fontWeight: 'bold',
|
||||
}"
|
||||
>
|
||||
{{ nation.name }}
|
||||
</div>
|
||||
<div class="type-head tb-head bg1">성향</div>
|
||||
<div class="type-body tb-body">
|
||||
{{ nation.type.name }} (<span style="color: cyan">{{ nation.type.pros }}</span>
|
||||
<span style="color: magenta">{{ nation.type.cons }}</span
|
||||
>)
|
||||
</div>
|
||||
<div class="c12-head tb-head bg1">{{ formatOfficerLevelText(12, nation.level) }}</div>
|
||||
<div class="c12-body tb-body" :style="{ color: getNPCColor(nation.topChiefs[12]?.npc ?? 1) }">
|
||||
{{ nation.topChiefs[12]?.name ?? "-" }}
|
||||
</div>
|
||||
<div class="c11-head tb-head bg1">{{ formatOfficerLevelText(11, nation.level) }}</div>
|
||||
<div class="c11-body tb-body" :style="{ color: getNPCColor(nation.topChiefs[11]?.npc ?? 1) }">
|
||||
{{ nation.topChiefs[11]?.name ?? "-" }}
|
||||
</div>
|
||||
<div class="pop-head tb-head bg1">총 주민</div>
|
||||
<div v-if="!nation.id" class="pop-body tb-body">해당 없음</div>
|
||||
<div v-else class="pop-body tb-body">
|
||||
{{ nation.population.now.toLocaleString() }} / {{ nation.population.max.toLocaleString() }}
|
||||
</div>
|
||||
<div class="crew-head tb-head bg1">총 병사</div>
|
||||
<div v-if="!nation.id" class="crew-body tb-body">해당 없음</div>
|
||||
<div v-else class="crew-body tb-body">
|
||||
{{ nation.crew.now.toLocaleString() }} / {{ nation.crew.max.toLocaleString() }}
|
||||
</div>
|
||||
<div class="gold-head tb-head bg1">국고</div>
|
||||
<div v-if="!nation.id" class="gold-body tb-body">해당 없음</div>
|
||||
<div v-else class="gold-body tb-body">{{ nation.gold.toLocaleString() }}</div>
|
||||
<div class="rice-head tb-head bg1">병량</div>
|
||||
<div v-if="!nation.id" class="rice-body tb-body">해당 없음</div>
|
||||
<div v-else class="rice-body tb-body">{{ nation.rice.toLocaleString() }}</div>
|
||||
<div class="bill-head tb-head bg1">지급률</div>
|
||||
<div v-if="!nation.id" class="bill-body tb-body">해당 없음</div>
|
||||
<div v-else class="bill-body tb-body">{{ nation.bill }}%</div>
|
||||
<div class="taxRate-head tb-head bg1">세율</div>
|
||||
<div v-if="!nation.id" class="taxRate-body tb-body">해당 없음</div>
|
||||
<div v-else class="taxRate-body tb-body">{{ nation.taxRate }}%</div>
|
||||
<div class="cityCnt-head tb-head bg1">속령</div>
|
||||
<div v-if="!nation.id" class="cityCnt-body tb-body">해당 없음</div>
|
||||
<div v-else class="cityCnt-body tb-body">{{ nation.population.cityCnt.toLocaleString() }}</div>
|
||||
<div class="genCnt-head tb-head bg1">장수</div>
|
||||
<div v-if="!nation.id" class="genCnt-body tb-body">해당 없음</div>
|
||||
<div v-else class="genCnt-body tb-body">{{ nation.crew.generalCnt.toLocaleString() }}</div>
|
||||
<div class="power-head tb-head bg1">국력</div>
|
||||
<div v-if="!nation.id" class="power-body tb-body">해당 없음</div>
|
||||
<div v-else class="power-body tb-body">{{ nation.power.toLocaleString() }}</div>
|
||||
<div class="tech-head tb-head bg1">기술력</div>
|
||||
<div v-if="!nation.id" class="tech-body tb-body">해당 없음</div>
|
||||
<div v-else class="tech-body tb-body">
|
||||
{{ currentTechLevel }}등급 /
|
||||
<span :style="{ color: onTechLimit ? 'magenta' : 'limegreen' }">{{
|
||||
Math.floor(nation.tech).toLocaleString()
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="strategicCmd-head tb-head bg1">전략</div>
|
||||
<div v-if="!nation.id" class="strategicCmd-body tb-body">해당 없음</div>
|
||||
<div
|
||||
v-else-if="impossibleStrategicCommandText"
|
||||
v-b-tooltip.hover
|
||||
class="strategicCmd-body tb-body"
|
||||
:title="impossibleStrategicCommandText"
|
||||
style="text-decoration: underline dashed red"
|
||||
>
|
||||
<span v-if="nation.strategicCmdLimit" style="color: red">{{ nation.strategicCmdLimit.toLocaleString() }}턴</span>
|
||||
<span v-else style="color: yellow">가능</span>
|
||||
</div>
|
||||
<div v-else class="strategicCmd-body tb-body">
|
||||
<span v-if="nation.strategicCmdLimit" style="color: red">{{ nation.strategicCmdLimit.toLocaleString() }}턴</span>
|
||||
<span v-else style="color: limegreen">가능</span>
|
||||
</div>
|
||||
<div class="diplomaticCmd-head tb-head bg1">외교</div>
|
||||
<div v-if="!nation.id" class="diplomaticCmd-body tb-body">해당 없음</div>
|
||||
<div v-else class="diplomaticCmd-body tb-body">
|
||||
<span v-if="nation.diplomaticLimit" style="color: red">{{ nation.diplomaticLimit.toLocaleString() }}턴</span>
|
||||
<span v-else style="color: limegreen">가능</span>
|
||||
</div>
|
||||
<div class="prohibitScout-head tb-head bg1">임관</div>
|
||||
<div v-if="!nation.id" class="prohibitScout-body tb-body">해당 없음</div>
|
||||
<div v-else class="prohibitScout-body tb-body">
|
||||
<span v-if="nation.prohibitScout" style="color: red">금지</span>
|
||||
<span v-else style="color: limegreen">허가</span>
|
||||
</div>
|
||||
<div class="prohibitWar-head tb-head bg1">전쟁</div>
|
||||
<div v-if="!nation.id" class="prohibitWar-body tb-body">해당 없음</div>
|
||||
<div v-else class="prohibitWar-body tb-body">
|
||||
<span v-if="nation.prohibitWar" style="color: red">금지</span>
|
||||
<span v-else style="color: limegreen">허가</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import type { GetFrontInfoResponse } from "@/defs/API/Global";
|
||||
import type { GameConstStore } from "@/GameConstStore";
|
||||
import { joinYearMonth } from "@/util/joinYearMonth";
|
||||
import { parseYearMonth } from "@/util/parseYearMonth";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
import { isBrightColor } from "@/util/isBrightColor";
|
||||
import { formatOfficerLevelText, getNPCColor, isTechLimited, convTechLevel, getMaxRelativeTechLevel } from "@/utilGame";
|
||||
import { inject, ref, toRef, watch, type Ref } from "vue";
|
||||
const props = defineProps<{
|
||||
nation: GetFrontInfoResponse["nation"];
|
||||
global: GetFrontInfoResponse["global"];
|
||||
}>();
|
||||
|
||||
const gameConstStore = unwrap(inject<Ref<GameConstStore>>("gameConstStore"));
|
||||
const nation = toRef(props, "nation");
|
||||
const global = toRef(props, "global");
|
||||
|
||||
const currentTechLevel = ref(0);
|
||||
const maxTechLevel = ref(0);
|
||||
const onTechLimit = ref(false);
|
||||
watch(
|
||||
nation,
|
||||
(nation) => {
|
||||
const { startyear, year } = global.value;
|
||||
console.log(gameConstStore);
|
||||
maxTechLevel.value = getMaxRelativeTechLevel(startyear, year, gameConstStore.value.gameConst.maxTechLevel);
|
||||
currentTechLevel.value = convTechLevel(nation.tech, maxTechLevel.value);
|
||||
onTechLimit.value = isTechLimited(startyear, year, nation.tech, gameConstStore.value.gameConst.maxTechLevel);
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
const impossibleStrategicCommandText = ref<string>("");
|
||||
watch(
|
||||
nation,
|
||||
(nation) => {
|
||||
if (nation.impossibleStrategicCommand.length == 0) {
|
||||
impossibleStrategicCommandText.value = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const yearMonth = joinYearMonth(global.value.year, global.value.month);
|
||||
const texts = [];
|
||||
for (const [cmdName, turnCnt] of nation.impossibleStrategicCommand) {
|
||||
const [year, month] = parseYearMonth(yearMonth + turnCnt);
|
||||
texts.push(`${cmdName}: ${turnCnt.toLocaleString()}턴 뒤(${year}년 ${month}월부터)`);
|
||||
}
|
||||
impossibleStrategicCommandText.value = texts.join("<br>\n");
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.nation-card-basic {
|
||||
width: 500px;
|
||||
height: 193px;
|
||||
display: grid;
|
||||
grid-template-columns: 7fr 18fr 7fr 18fr;
|
||||
grid-template-rows: repeat(10, calc(192px / 10));
|
||||
|
||||
border-bottom: solid 1px gray;
|
||||
border-right: solid 1px gray;
|
||||
|
||||
.name {
|
||||
grid-column: 1 / span 4;
|
||||
}
|
||||
|
||||
.type-body {
|
||||
grid-column: 2 / span 3;
|
||||
}
|
||||
}
|
||||
|
||||
.tb-title {
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
line-height: calc(193px / 10);
|
||||
|
||||
border-left: solid 1px gray;
|
||||
border-top: solid 1px gray;
|
||||
}
|
||||
.tb-head {
|
||||
border-left: solid 1px gray;
|
||||
border-top: solid 1px gray;
|
||||
|
||||
text-align: center;
|
||||
padding: 0px;
|
||||
line-height: calc(193px / 10);
|
||||
}
|
||||
|
||||
.tb-body {
|
||||
border-top: solid 1px gray;
|
||||
padding: 0px;
|
||||
line-height: calc(193px / 10);
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user