feat: complete legacy game screen parity
This commit is contained in:
@@ -29,37 +29,52 @@ const props = defineProps<{
|
||||
</div>
|
||||
<div v-else-if="!props.city" class="empty">도시 정보를 불러오지 못했습니다.</div>
|
||||
<div v-else class="city-body">
|
||||
<div class="title">{{ props.city.name }} (Lv {{ props.city.level }})</div>
|
||||
<div class="title">
|
||||
{{ props.city.name }} (Lv {{ props.city.level }}) · 국가 {{ props.city.nationId || '무주' }}
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>인구 {{ props.city.population }}</div>
|
||||
<div>농업 {{ props.city.agriculture }}</div>
|
||||
<div>상업 {{ props.city.commerce }}</div>
|
||||
<div>치안 {{ props.city.security }}</div>
|
||||
<div>방어 {{ props.city.defence }}</div>
|
||||
<div>성벽 {{ props.city.wall }}</div>
|
||||
<div>보급 {{ props.city.supplyState }}</div>
|
||||
<div>전방 {{ props.city.frontState }}</div>
|
||||
<span>인구</span><strong>{{ props.city.population.toLocaleString() }}</strong> <span>농업</span
|
||||
><strong>{{ props.city.agriculture.toLocaleString() }}</strong> <span>상업</span
|
||||
><strong>{{ props.city.commerce.toLocaleString() }}</strong> <span>치안</span
|
||||
><strong>{{ props.city.security.toLocaleString() }}</strong> <span>수비</span
|
||||
><strong>{{ props.city.defence.toLocaleString() }}</strong> <span>성벽</span
|
||||
><strong>{{ props.city.wall.toLocaleString() }}</strong> <span>보급</span
|
||||
><strong>{{ props.city.supplyState }}</strong> <span>전방</span
|
||||
><strong>{{ props.city.frontState }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.city-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
min-height: 24px;
|
||||
padding: 2px 6px;
|
||||
border-bottom: 1px solid #666;
|
||||
background: #173d27;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
font-size: 12px;
|
||||
}
|
||||
.grid > * {
|
||||
min-height: 23px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.grid > span {
|
||||
background: rgb(20 75 42 / 70%);
|
||||
text-align: center;
|
||||
}
|
||||
.grid > strong {
|
||||
text-align: right;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@@ -150,8 +150,7 @@ const clearNationTurn = (index: number) => {
|
||||
emit('set-nation-turn', { index, action: '휴식', args: {} });
|
||||
};
|
||||
|
||||
const canNationReserve = () =>
|
||||
Boolean(props.general && props.general.nationId > 0 && props.general.officerLevel >= 5);
|
||||
const canNationReserve = () => Boolean(props.general && props.general.nationId > 0 && props.general.officerLevel >= 5);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -160,7 +159,8 @@ const canNationReserve = () =>
|
||||
<div class="label">선택 도시</div>
|
||||
<div class="value">
|
||||
<span v-if="props.selectedCity">
|
||||
{{ props.selectedCity.name }} · {{ props.selectedCity.nationName }} · {{ props.selectedCity.regionName }}
|
||||
{{ props.selectedCity.name }} · {{ props.selectedCity.nationName }} ·
|
||||
{{ props.selectedCity.regionName }}
|
||||
</span>
|
||||
<span v-else>선택된 도시 없음</span>
|
||||
</div>
|
||||
@@ -260,24 +260,32 @@ const canNationReserve = () =>
|
||||
}
|
||||
|
||||
.command-selection {
|
||||
border: 1px solid rgba(201, 164, 90, 0.35);
|
||||
padding: 6px 8px;
|
||||
font-size: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
display: grid;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
min-height: 24px;
|
||||
border: 1px solid #666;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.command-selection .label {
|
||||
color: rgba(232, 221, 196, 0.6);
|
||||
padding: 2px 5px;
|
||||
background: #173d27;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
}
|
||||
.command-selection .value {
|
||||
overflow: hidden;
|
||||
padding: 2px 5px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.command-selected {
|
||||
border: 1px solid rgba(201, 164, 90, 0.3);
|
||||
padding: 8px;
|
||||
border: 1px solid #666;
|
||||
padding: 3px 5px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
font-size: 0.75rem;
|
||||
grid-template-columns: 64px minmax(0, 1fr);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.command-selected .label {
|
||||
|
||||
@@ -98,18 +98,8 @@ watch(selectedCategory, (value) => {
|
||||
}
|
||||
});
|
||||
|
||||
const statusLabel = (command: CommandAvailability) => {
|
||||
if (command.status === 'available') {
|
||||
return '가능';
|
||||
}
|
||||
if (command.status === 'needsInput') {
|
||||
return '입력 필요';
|
||||
}
|
||||
if (command.status === 'blocked') {
|
||||
return '불가';
|
||||
}
|
||||
return '확인 필요';
|
||||
};
|
||||
const commandTitle = (command: CommandAvailability) =>
|
||||
command.reason || (command.reqArg ? '대상을 선택하는 명령입니다.' : command.possible ? '실행 가능' : '실행 불가');
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -140,10 +130,10 @@ const statusLabel = (command: CommandAvailability) => {
|
||||
command.status === 'blocked' ? 'blocked' : '',
|
||||
]"
|
||||
:disabled="!command.possible"
|
||||
:title="commandTitle(command)"
|
||||
@click="emit('select', command.key)"
|
||||
>
|
||||
<span class="command-name">{{ command.name }}</span>
|
||||
<span class="command-status">{{ statusLabel(command) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -154,49 +144,63 @@ const statusLabel = (command: CommandAvailability) => {
|
||||
.command-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
gap: 0;
|
||||
border-top: 1px solid #666;
|
||||
border-left: 1px solid #666;
|
||||
}
|
||||
|
||||
.category-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.category-btn {
|
||||
border: 1px solid rgba(201, 164, 90, 0.4);
|
||||
padding: 6px 8px;
|
||||
font-size: 0.75rem;
|
||||
min-height: 24px;
|
||||
border: 0;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
padding: 2px 4px;
|
||||
background: #173d27;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.category-btn.active {
|
||||
background: rgba(201, 164, 90, 0.2);
|
||||
background: #28633f;
|
||||
color: #ffe38a;
|
||||
}
|
||||
|
||||
.command-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(130px, 1fr));
|
||||
gap: 6px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
border: 1px solid rgba(201, 164, 90, 0.3);
|
||||
padding: 6px;
|
||||
min-height: 24px;
|
||||
border: 0;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
padding: 2px 5px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
text-align: left;
|
||||
font-size: 0.75rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #302016 var(--sammo-texture-walnut);
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.command-item.ok {
|
||||
border-color: rgba(201, 164, 90, 0.6);
|
||||
color: #d9f7df;
|
||||
}
|
||||
|
||||
.command-item.blocked {
|
||||
opacity: 0.5;
|
||||
color: #888;
|
||||
opacity: 0.72;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
@@ -204,11 +208,6 @@ const statusLabel = (command: CommandAvailability) => {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.command-status {
|
||||
font-size: 0.7rem;
|
||||
color: rgba(232, 221, 196, 0.6);
|
||||
}
|
||||
|
||||
.empty {
|
||||
color: rgba(232, 221, 196, 0.6);
|
||||
}
|
||||
|
||||
@@ -21,6 +21,11 @@ interface GeneralInfo {
|
||||
injury: number;
|
||||
experience: number;
|
||||
dedication: number;
|
||||
age?: number;
|
||||
turnTime?: string;
|
||||
crewTypeId?: number;
|
||||
traits?: { personal: string; specialWar: string; specialDomestic: string };
|
||||
progression?: { experienceLevel: number; dedicationLevel: number; dex: number[] };
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -36,61 +41,71 @@ const props = defineProps<{
|
||||
</div>
|
||||
<div v-else-if="!props.general" class="empty">장수 정보를 불러오지 못했습니다.</div>
|
||||
<div v-else class="general-body">
|
||||
<div class="general-header">
|
||||
<span class="name">{{ props.general.name }}</span>
|
||||
<span class="meta">ID {{ props.general.id }} · 관직 {{ props.general.officerLevel }}</span>
|
||||
<div class="general-title">
|
||||
{{ props.general.name }} · 관직 {{ props.general.officerLevel }} · {{ props.general.age ?? '-' }}세
|
||||
</div>
|
||||
<div class="stats">
|
||||
<div>통솔 {{ props.general.stats.leadership }}</div>
|
||||
<div>무력 {{ props.general.stats.strength }}</div>
|
||||
<div>지력 {{ props.general.stats.intelligence }}</div>
|
||||
</div>
|
||||
<div class="resources">
|
||||
<div>금 {{ props.general.gold }}</div>
|
||||
<div>쌀 {{ props.general.rice }}</div>
|
||||
<div>병 {{ props.general.crew }}</div>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div>훈련 {{ props.general.train }}</div>
|
||||
<div>사기 {{ props.general.atmos }}</div>
|
||||
<div>부상 {{ props.general.injury }}</div>
|
||||
<div>경험 {{ props.general.experience }}</div>
|
||||
<div>공헌 {{ props.general.dedication }}</div>
|
||||
<div class="legacy-grid">
|
||||
<span>통솔</span><strong>{{ props.general.stats.leadership }}</strong> <span>무력</span
|
||||
><strong>{{ props.general.stats.strength }}</strong> <span>지력</span
|
||||
><strong>{{ props.general.stats.intelligence }}</strong> <span>자금</span
|
||||
><strong>{{ props.general.gold.toLocaleString() }}</strong> <span>군량</span
|
||||
><strong>{{ props.general.rice.toLocaleString() }}</strong> <span>병력</span
|
||||
><strong>{{ props.general.crew.toLocaleString() }}</strong> <span>훈련</span
|
||||
><strong>{{ props.general.train }}</strong> <span>사기</span><strong>{{ props.general.atmos }}</strong>
|
||||
<span>부상</span><strong>{{ props.general.injury }}</strong> <span>명망</span
|
||||
><strong
|
||||
>Lv {{ props.general.progression?.experienceLevel ?? 0 }} ({{ props.general.experience }})</strong
|
||||
>
|
||||
<span>계급</span
|
||||
><strong
|
||||
>Lv {{ props.general.progression?.dedicationLevel ?? 0 }} ({{ props.general.dedication }})</strong
|
||||
>
|
||||
<span>병종</span><strong>{{ props.general.crewTypeId || '-' }}</strong> <span>성격</span
|
||||
><strong>{{ props.general.traits?.personal ?? '-' }}</strong> <span>전투특기</span
|
||||
><strong>{{ props.general.traits?.specialWar ?? '-' }}</strong> <span>내정특기</span
|
||||
><strong>{{ props.general.traits?.specialDomestic ?? '-' }}</strong> <span>다음 턴</span
|
||||
><strong>{{ props.general.turnTime?.slice(11, 16) ?? '-' }}</strong>
|
||||
</div>
|
||||
<div class="dex">숙련도 {{ props.general.progression?.dex?.join(' / ') ?? '0 / 0 / 0 / 0 / 0' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.general-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
.general-title {
|
||||
min-height: 24px;
|
||||
padding: 2px 6px;
|
||||
border-bottom: 1px solid #777;
|
||||
background: #173d27;
|
||||
text-align: center;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.general-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.general-header .name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.general-header .meta {
|
||||
font-size: 0.75rem;
|
||||
color: rgba(232, 221, 196, 0.7);
|
||||
}
|
||||
|
||||
.stats,
|
||||
.resources,
|
||||
.status {
|
||||
.legacy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
font-size: 12px;
|
||||
}
|
||||
.legacy-grid > * {
|
||||
min-height: 22px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
padding: 2px 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.legacy-grid > span {
|
||||
background: rgb(20 75 42 / 70%);
|
||||
text-align: center;
|
||||
}
|
||||
.legacy-grid > strong {
|
||||
text-align: right;
|
||||
font-weight: 400;
|
||||
}
|
||||
.dex {
|
||||
padding: 3px 6px;
|
||||
font-size: 12px;
|
||||
color: #ddd;
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@@ -36,8 +36,9 @@ defineProps<{
|
||||
|
||||
<style scoped>
|
||||
.front-status {
|
||||
width: calc(100% + 48px);
|
||||
margin-left: -24px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
margin-left: 0;
|
||||
background-color: #302016;
|
||||
background-image: var(--sammo-texture-walnut);
|
||||
color: #fff;
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type NationNavigationAccess,
|
||||
} from './mainNavigation';
|
||||
import { useMenuPopup } from './useMenuPopup';
|
||||
import { legacyNationTextColor } from '../../utils/legacyNationColor';
|
||||
|
||||
const props = defineProps<{
|
||||
access: NationNavigationAccess;
|
||||
@@ -23,6 +24,7 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
|
||||
:ref="setRoot"
|
||||
class="main-nation-menu"
|
||||
:style="{ '--nation-menu-color': nationColor || '#000000' }"
|
||||
:class="{ 'dark-label': legacyNationTextColor(nationColor) === '#000000' }"
|
||||
aria-label="국가 메뉴"
|
||||
>
|
||||
<template v-for="entry in nationNavigation" :key="entry.id">
|
||||
@@ -83,6 +85,10 @@ const isActive = (link: MainNavigationLinkItem) => link.highlightStage === props
|
||||
background-color: var(--nation-menu-color);
|
||||
background-image: none;
|
||||
}
|
||||
.main-nation-menu.dark-label :deep(.main-menu-link),
|
||||
.main-nation-menu.dark-label .main-menu-button {
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.nation-menu-split {
|
||||
position: relative;
|
||||
|
||||
@@ -280,18 +280,11 @@ const selectCity = (cityId: number) => {
|
||||
<div class="map-viewer">
|
||||
<div class="map-top">
|
||||
<div class="map-title">{{ mapSummary }}</div>
|
||||
<div class="map-controls">
|
||||
<button class="map-toggle" :class="{ active: showCityName }" @click="mapStore.toggleCityName">
|
||||
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="props.loading">
|
||||
<SkeletonLines :lines="4" />
|
||||
</div>
|
||||
<div v-else-if="!props.mapData || !props.mapLayout" class="map-empty">
|
||||
지도 데이터를 불러오지 못했습니다.
|
||||
</div>
|
||||
<div v-else-if="!props.mapData || !props.mapLayout" class="map-empty">지도 데이터를 불러오지 못했습니다.</div>
|
||||
<div v-else ref="mapBody" class="map-body">
|
||||
<div
|
||||
ref="mapArea"
|
||||
@@ -324,14 +317,11 @@ const selectCity = (cityId: number) => {
|
||||
{{ hoveredCity.nationName }} · {{ hoveredCity.regionName }} · {{ hoveredCity.levelName }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="map-meta">
|
||||
<span>도시 {{ props.mapData.cityList.length }}</span>
|
||||
<span>세력 {{ props.mapData.nationList.length }}</span>
|
||||
<span>테마 {{ props.mapLayout.mapName }}</span>
|
||||
</div>
|
||||
<div class="map-footnote">
|
||||
좌표/도시명은 시나리오 맵 레이아웃을 기준으로 표시됩니다.
|
||||
<div class="map-controls">
|
||||
<button class="map-toggle" :class="{ active: showCityName }" @click="mapStore.toggleCityName">
|
||||
도시명 표기 {{ showCityName ? '끄기' : '켜기' }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -423,19 +413,6 @@ const selectCity = (cityId: number) => {
|
||||
color: rgba(232, 221, 196, 0.6);
|
||||
}
|
||||
|
||||
.map-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
font-size: 0.75rem;
|
||||
color: rgba(232, 221, 196, 0.6);
|
||||
}
|
||||
|
||||
.map-footnote {
|
||||
font-size: 0.65rem;
|
||||
color: rgba(232, 221, 196, 0.5);
|
||||
}
|
||||
|
||||
.map-empty {
|
||||
color: rgba(232, 221, 196, 0.6);
|
||||
}
|
||||
|
||||
@@ -377,7 +377,8 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
min-height: 22px;
|
||||
min-height: 0;
|
||||
padding: 2px 7px;
|
||||
}
|
||||
|
||||
.MessageList {
|
||||
@@ -426,7 +427,7 @@ const forwardResponse = (messageId: number, response: boolean) => {
|
||||
}
|
||||
|
||||
.MessageList {
|
||||
height: 650px;
|
||||
max-height: 650px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import SkeletonLines from '../ui/SkeletonLines.vue';
|
||||
import { legacyNationTextColor } from '../../utils/legacyNationColor';
|
||||
|
||||
interface NationInfo {
|
||||
id: number;
|
||||
@@ -26,46 +27,50 @@ const props = defineProps<{
|
||||
</div>
|
||||
<div v-else-if="!props.nation" class="empty">국가 정보를 불러오지 못했습니다.</div>
|
||||
<div v-else class="nation-body">
|
||||
<div class="title">
|
||||
<span class="color" :style="{ backgroundColor: props.nation.color }" />
|
||||
<div
|
||||
class="title"
|
||||
:style="{ backgroundColor: props.nation.color, color: legacyNationTextColor(props.nation.color) }"
|
||||
>
|
||||
{{ props.nation.name }} (Lv {{ props.nation.level }})
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div>국고 {{ props.nation.gold }}</div>
|
||||
<div>국량 {{ props.nation.rice }}</div>
|
||||
<div>기술 {{ props.nation.tech }}</div>
|
||||
<div>체제 {{ props.nation.typeCode }}</div>
|
||||
<div>수도 {{ props.nation.capitalCityId ?? '-' }}</div>
|
||||
<span>국고</span><strong>{{ props.nation.gold.toLocaleString() }}</strong> <span>국량</span
|
||||
><strong>{{ props.nation.rice.toLocaleString() }}</strong> <span>기술</span
|
||||
><strong>{{ props.nation.tech.toLocaleString() }}</strong> <span>체제</span
|
||||
><strong>{{ props.nation.typeCode }}</strong> <span>수도</span
|
||||
><strong>{{ props.nation.capitalCityId ?? '-' }}</strong> <span>국가 등급</span
|
||||
><strong>{{ props.nation.level }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.nation-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 24px;
|
||||
padding: 2px 6px;
|
||||
text-align: center;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.color {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1px solid rgba(232, 221, 196, 0.6);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(90px, 1fr));
|
||||
gap: 6px;
|
||||
font-size: 0.85rem;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
font-size: 12px;
|
||||
}
|
||||
.grid > * {
|
||||
min-height: 23px;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
.grid > span {
|
||||
background: rgb(20 75 42 / 70%);
|
||||
text-align: center;
|
||||
}
|
||||
.grid > strong {
|
||||
text-align: right;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, watch } from 'vue';
|
||||
import { EditorContent, useEditor } from '@tiptap/vue-3';
|
||||
import StarterKit from '@tiptap/starter-kit';
|
||||
import Underline from '@tiptap/extension-underline';
|
||||
import Link from '@tiptap/extension-link';
|
||||
|
||||
const props = withDefaults(defineProps<{ modelValue: string; maxLength?: number }>(), { maxLength: 16384 });
|
||||
const emit = defineEmits<{ (event: 'update:modelValue', value: string): void }>();
|
||||
|
||||
const editor = useEditor({
|
||||
content: props.modelValue,
|
||||
extensions: [StarterKit, Underline, Link.configure({ openOnClick: false })],
|
||||
editorProps: {
|
||||
attributes: { class: 'legacy-html-editor__content', 'aria-label': 'HTML 편집기' },
|
||||
},
|
||||
onUpdate: ({ editor: instance }) => {
|
||||
const html = instance.getHTML();
|
||||
if (html.length <= props.maxLength) emit('update:modelValue', html);
|
||||
},
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
if (editor.value && editor.value.getHTML() !== value) {
|
||||
editor.value.commands.setContent(value || '', { emitUpdate: false });
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const setLink = () => {
|
||||
const previous = editor.value?.getAttributes('link').href as string | undefined;
|
||||
const href = window.prompt('링크 주소', previous ?? 'https://');
|
||||
if (href === null || !editor.value) return;
|
||||
if (!href.trim()) editor.value.chain().focus().unsetLink().run();
|
||||
else editor.value.chain().focus().extendMarkRange('link').setLink({ href: href.trim() }).run();
|
||||
};
|
||||
|
||||
onBeforeUnmount(() => editor.value?.destroy());
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="legacy-html-editor">
|
||||
<div class="legacy-html-editor__toolbar" role="toolbar" aria-label="서식">
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: editor?.isActive('bold') }"
|
||||
@click="editor?.chain().focus().toggleBold().run()"
|
||||
>
|
||||
<b>굵게</b>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: editor?.isActive('italic') }"
|
||||
@click="editor?.chain().focus().toggleItalic().run()"
|
||||
>
|
||||
<i>기울임</i>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: editor?.isActive('underline') }"
|
||||
@click="editor?.chain().focus().toggleUnderline().run()"
|
||||
>
|
||||
<u>밑줄</u>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
:class="{ active: editor?.isActive('bulletList') }"
|
||||
@click="editor?.chain().focus().toggleBulletList().run()"
|
||||
>
|
||||
목록
|
||||
</button>
|
||||
<button type="button" :class="{ active: editor?.isActive('link') }" @click="setLink">링크</button>
|
||||
<button type="button" @click="editor?.chain().focus().unsetAllMarks().clearNodes().run()">
|
||||
서식 지우기
|
||||
</button>
|
||||
</div>
|
||||
<EditorContent :editor="editor" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.legacy-html-editor {
|
||||
border: 1px solid #777;
|
||||
background: #fff;
|
||||
color: #111;
|
||||
}
|
||||
.legacy-html-editor__toolbar {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid #aaa;
|
||||
padding: 3px;
|
||||
background: #ddd;
|
||||
}
|
||||
.legacy-html-editor__toolbar button {
|
||||
border: 1px solid #777;
|
||||
border-radius: 2px;
|
||||
padding: 2px 7px;
|
||||
background: #f5f5f5;
|
||||
color: #111;
|
||||
cursor: pointer;
|
||||
}
|
||||
.legacy-html-editor__toolbar button.active {
|
||||
background: #b9d4f0;
|
||||
}
|
||||
:deep(.legacy-html-editor__content) {
|
||||
min-height: 110px;
|
||||
padding: 6px;
|
||||
outline: none;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
:deep(.legacy-html-editor__content p) {
|
||||
margin: 0 0 0.4em;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user