465 lines
12 KiB
Vue
465 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import { computed, reactive } from 'vue';
|
|
import type { MessageType } from '@sammo-ts/logic';
|
|
import SkeletonLines from '../ui/SkeletonLines.vue';
|
|
import MessagePlate from './MessagePlate.vue';
|
|
|
|
interface MessageTarget {
|
|
generalId: number;
|
|
generalName: string;
|
|
nationId: number;
|
|
nationName: string;
|
|
color: string;
|
|
icon: string;
|
|
}
|
|
|
|
interface MessageEntry {
|
|
id: number;
|
|
text: string;
|
|
time: string;
|
|
msgType: MessageType;
|
|
src: MessageTarget;
|
|
dest: MessageTarget | null;
|
|
option?: Record<string, unknown> | null;
|
|
}
|
|
|
|
interface MessageBucket {
|
|
private: MessageEntry[];
|
|
public: MessageEntry[];
|
|
national: MessageEntry[];
|
|
diplomacy: MessageEntry[];
|
|
permission: number;
|
|
latestRead: {
|
|
private: number;
|
|
diplomacy: number;
|
|
};
|
|
}
|
|
|
|
interface MailboxGroup {
|
|
label: string;
|
|
color?: string;
|
|
options: Array<{
|
|
label: string;
|
|
value: number;
|
|
disabled?: boolean;
|
|
color?: string;
|
|
}>;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
messages: MessageBucket | null;
|
|
loading: boolean;
|
|
targetMailbox: number;
|
|
draftText: string;
|
|
mailboxGroups: MailboxGroup[];
|
|
generalId: number;
|
|
generalName: string;
|
|
nationId: number;
|
|
canRespondDiplomacy: boolean;
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
(event: 'update:targetMailbox', value: number): void;
|
|
(event: 'update:draftText', value: string): void;
|
|
(event: 'send'): void;
|
|
(event: 'refresh'): void;
|
|
(event: 'load-older', type: MessageType): void;
|
|
(event: 'respond', messageId: number, response: boolean): void;
|
|
(event: 'read-latest', type: 'private' | 'diplomacy', messageId: number): void;
|
|
(event: 'delete', messageId: number): void;
|
|
}>();
|
|
|
|
const sections: Array<{ type: MessageType; label: string; className: string }> = [
|
|
{ type: 'public', label: '전체 메시지', className: 'PublicTalk' },
|
|
{ type: 'national', label: '국가 메시지', className: 'NationalTalk' },
|
|
{ type: 'private', label: '개인 메시지', className: 'PrivateTalk' },
|
|
{ type: 'diplomacy', label: '외교 메시지', className: 'DiplomacyTalk' },
|
|
];
|
|
|
|
const visibleLimits = reactive<Record<MessageType, number>>({
|
|
public: Number.POSITIVE_INFINITY,
|
|
national: Number.POSITIVE_INFINITY,
|
|
private: Number.POSITIVE_INFINITY,
|
|
diplomacy: Number.POSITIVE_INFINITY,
|
|
});
|
|
|
|
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);
|
|
emit('update:targetMailbox', Number.isFinite(parsed) ? parsed : 0);
|
|
};
|
|
|
|
const submit = () => {
|
|
if (!props.draftText.trim()) {
|
|
emit('refresh');
|
|
return;
|
|
}
|
|
emit('send');
|
|
};
|
|
|
|
const newestIncomingId = (type: 'private' | 'diplomacy'): number =>
|
|
bucket(type)
|
|
.filter((message) => message.src.generalId !== props.generalId)
|
|
.reduce((latest, message) => Math.max(latest, message.id), 0);
|
|
|
|
const canMarkRead = (type: 'private' | 'diplomacy'): boolean => {
|
|
if (!props.messages) {
|
|
return false;
|
|
}
|
|
const newest = newestIncomingId(type);
|
|
return newest > props.messages.latestRead[type];
|
|
};
|
|
|
|
const markRead = (type: 'private' | 'diplomacy') => {
|
|
const messageId = newestIncomingId(type);
|
|
if (messageId > 0) {
|
|
emit('read-latest', type, messageId);
|
|
}
|
|
};
|
|
|
|
const setSectionMailbox = (type: MessageType) => {
|
|
if (type === 'public') {
|
|
emit('update:targetMailbox', 9999);
|
|
} else if (type === 'national') {
|
|
emit('update:targetMailbox', 9000 + props.nationId);
|
|
}
|
|
};
|
|
|
|
const setReplyTarget = (type: MessageType, target: MessageTarget) => {
|
|
const mailbox =
|
|
(type === 'diplomacy' || type === 'national') && target.nationId !== props.nationId
|
|
? 9000 + target.nationId
|
|
: target.generalId;
|
|
if (mailbox > 0) {
|
|
emit('update:targetMailbox', mailbox);
|
|
}
|
|
};
|
|
|
|
const fold = (type: MessageType) => {
|
|
if (bucket(type).length >= 10) {
|
|
visibleLimits[type] = 10;
|
|
}
|
|
};
|
|
|
|
const forwardResponse = (messageId: number, response: boolean) => {
|
|
emit('respond', messageId, response);
|
|
};
|
|
</script>
|
|
|
|
<template>
|
|
<div class="MessagePanel">
|
|
<div class="MessageInputForm">
|
|
<div id="mailbox_list-col">
|
|
<select
|
|
id="mailbox_list"
|
|
class="message-select"
|
|
:value="targetMailbox"
|
|
aria-label="메시지 수신 대상"
|
|
@change="setMailbox(($event.target as HTMLSelectElement).value)"
|
|
>
|
|
<optgroup
|
|
v-for="group in mailboxGroups"
|
|
:key="group.label"
|
|
:label="group.label"
|
|
:style="{ backgroundColor: group.color ?? '#000000', color: '#ffffff' }"
|
|
>
|
|
<option
|
|
v-for="option in group.options"
|
|
:key="`${group.label}-${option.value}`"
|
|
:value="option.value"
|
|
:disabled="option.disabled"
|
|
:style="{ backgroundColor: option.color ?? '#000000', color: '#ffffff' }"
|
|
>
|
|
{{ option.label }}
|
|
</option>
|
|
</optgroup>
|
|
</select>
|
|
</div>
|
|
<div id="msg_input-col">
|
|
<input
|
|
class="message-text"
|
|
type="text"
|
|
maxlength="99"
|
|
:value="draftText"
|
|
aria-label="메시지 입력"
|
|
@input="emit('update:draftText', ($event.target as HTMLInputElement).value)"
|
|
@keydown.enter="submit"
|
|
/>
|
|
</div>
|
|
<div id="msg_submit-col">
|
|
<button class="message-send" type="button" @click="submit">서신전달&갱신</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="loading && !messages" class="message-loading">
|
|
<SkeletonLines :lines="4" />
|
|
</div>
|
|
|
|
<template v-else>
|
|
<section
|
|
v-for="section in sections"
|
|
:key="section.type"
|
|
:class="['message-section', section.className]"
|
|
:data-message-type="section.type"
|
|
>
|
|
<div class="stickyAnchor"></div>
|
|
<header class="BoardHeader">
|
|
<div class="header-label">{{ section.label }}</div>
|
|
<button
|
|
v-if="section.type === 'public' || section.type === 'national'"
|
|
class="btn-more-small action-primary"
|
|
type="button"
|
|
@click="setSectionMailbox(section.type)"
|
|
>
|
|
↩ 여기로
|
|
</button>
|
|
<button
|
|
v-else
|
|
class="btn-more-small action-secondary"
|
|
type="button"
|
|
:disabled="!canMarkRead(section.type)"
|
|
@click="markRead(section.type)"
|
|
>
|
|
모두 읽음
|
|
</button>
|
|
</header>
|
|
|
|
<div v-if="bucket(section.type).length === 0" class="empty-message">메시지가 없습니다.</div>
|
|
<div v-else class="MessageList">
|
|
<MessagePlate
|
|
v-for="message in visibleMessages(section.type)"
|
|
:key="message.id"
|
|
:message="message"
|
|
:general-id="generalId"
|
|
:general-name="generalName"
|
|
:nation-id="nationId"
|
|
:permission="permission"
|
|
:can-respond-diplomacy="canRespondDiplomacy"
|
|
:replyable-general-ids="replyableGeneralIds"
|
|
@set-target="setReplyTarget"
|
|
@delete="emit('delete', $event)"
|
|
@respond="forwardResponse"
|
|
/>
|
|
<div class="Actions">
|
|
<button class="fold-message" type="button" @click="fold(section.type)">접기</button>
|
|
<button class="load-older" type="button" @click="emit('load-older', section.type)">
|
|
이전 메시지 불러오기
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.MessagePanel {
|
|
color: #fff;
|
|
font-size: 14px;
|
|
}
|
|
|
|
.MessageInputForm {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) minmax(0, 4fr) minmax(0, 1fr);
|
|
grid-template-areas: 'mailbox input submit';
|
|
background-color: #302016;
|
|
background-image: var(--sammo-texture-walnut);
|
|
}
|
|
|
|
#mailbox_list-col {
|
|
grid-area: mailbox;
|
|
}
|
|
|
|
#msg_input-col {
|
|
grid-area: input;
|
|
}
|
|
|
|
#msg_submit-col {
|
|
grid-area: submit;
|
|
}
|
|
|
|
#mailbox_list-col,
|
|
#msg_input-col,
|
|
#msg_submit-col {
|
|
display: grid;
|
|
}
|
|
|
|
.message-select,
|
|
.message-text,
|
|
.message-send {
|
|
height: 35.5px;
|
|
border: 1px solid #6c757d;
|
|
border-radius: 4px;
|
|
font: inherit;
|
|
}
|
|
|
|
.message-select {
|
|
width: 100%;
|
|
background-color: #212529;
|
|
padding: 4px 30px 4px 12px;
|
|
color: #fff;
|
|
font-weight: 700;
|
|
}
|
|
|
|
.message-text {
|
|
width: 100%;
|
|
background-color: #fff;
|
|
padding: 4px 8px;
|
|
color: #212529;
|
|
}
|
|
|
|
.message-send,
|
|
.action-primary {
|
|
background-color: #337ab7;
|
|
color: #fff;
|
|
}
|
|
|
|
.message-send {
|
|
padding: 4px 8px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.message-send:hover {
|
|
background-color: #375a7f;
|
|
}
|
|
|
|
.message-send:focus,
|
|
.message-send:focus-visible {
|
|
outline: none !important;
|
|
outline-width: 0 !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.message-loading {
|
|
padding: 8px;
|
|
}
|
|
|
|
.message-section {
|
|
min-width: 0;
|
|
}
|
|
|
|
.BoardHeader {
|
|
display: flex;
|
|
min-height: 25px;
|
|
align-items: center;
|
|
outline: 1px solid gray;
|
|
background-color: #302016;
|
|
background-image: var(--sammo-texture-walnut);
|
|
color: #fff;
|
|
}
|
|
|
|
.header-label {
|
|
flex: 1;
|
|
}
|
|
|
|
.btn-more-small {
|
|
margin: 1px;
|
|
border: 1px solid transparent;
|
|
border-radius: 3px;
|
|
padding: 2px 6px;
|
|
font-size: 11.2px;
|
|
line-height: 1.5;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.action-secondary {
|
|
border-color: #6c757d;
|
|
background-color: #6c757d;
|
|
color: #fff;
|
|
}
|
|
|
|
.btn-more-small:disabled {
|
|
cursor: default;
|
|
opacity: 0.65;
|
|
}
|
|
|
|
.empty-message {
|
|
min-height: 0;
|
|
padding: 2px 7px;
|
|
}
|
|
|
|
.MessageList {
|
|
overflow-x: hidden;
|
|
}
|
|
|
|
.Actions {
|
|
display: grid;
|
|
}
|
|
|
|
.fold-message,
|
|
.load-older {
|
|
border: 1px solid transparent;
|
|
padding: 6px 12px;
|
|
color: #fff;
|
|
font: inherit;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.fold-message {
|
|
background-color: #212529;
|
|
}
|
|
|
|
.load-older {
|
|
background-color: #6c757d;
|
|
}
|
|
|
|
@media (min-width: 940px) {
|
|
.MessagePanel {
|
|
display: grid;
|
|
grid-template-columns: 1fr 1fr;
|
|
}
|
|
|
|
.MessageInputForm,
|
|
.message-loading {
|
|
grid-column: 1 / 3;
|
|
}
|
|
|
|
.PublicTalk,
|
|
.PrivateTalk {
|
|
border-right: 1px solid gray;
|
|
}
|
|
|
|
.fold-message {
|
|
display: none;
|
|
}
|
|
|
|
.MessageList {
|
|
max-height: 650px;
|
|
overflow-y: auto;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 939.98px) {
|
|
.MessageInputForm {
|
|
position: sticky;
|
|
z-index: 5;
|
|
top: 0;
|
|
grid-template-columns: 1fr 1fr;
|
|
grid-template-areas:
|
|
'mailbox submit'
|
|
'input input';
|
|
}
|
|
|
|
.message-text {
|
|
height: 33.5px;
|
|
}
|
|
|
|
.BoardHeader {
|
|
position: sticky;
|
|
z-index: 4;
|
|
top: 62px;
|
|
}
|
|
}
|
|
</style>
|