Merge branch 'main' into feature/command-option-context-20260813
This commit is contained in:
@@ -168,6 +168,14 @@ const closePicker = () => {
|
||||
quickTarget.value = null;
|
||||
selectedCommand.value = null;
|
||||
};
|
||||
const togglePicker = (turnIndex?: number) => {
|
||||
const target = turnIndex ?? null;
|
||||
if (pickerOpen.value && quickTarget.value === target) {
|
||||
closePicker();
|
||||
return;
|
||||
}
|
||||
openPicker(turnIndex);
|
||||
};
|
||||
const selectCommand = (commandKey: string) => {
|
||||
const command = props.commandTable?.[props.scope]
|
||||
.flatMap((group) => group.values)
|
||||
@@ -332,6 +340,7 @@ const clickOutsideMenu = (event: Event) => {
|
||||
>
|
||||
짝수턴
|
||||
</button>
|
||||
<hr class="menu-divider" />
|
||||
<template v-for="step in [3, 4, 5, 6, 7]" :key="step">
|
||||
<small>{{ step }}턴 간격</small>
|
||||
<div class="step-buttons">
|
||||
@@ -456,6 +465,7 @@ const clickOutsideMenu = (event: Event) => {
|
||||
>
|
||||
붙여넣기
|
||||
</button>
|
||||
<hr class="menu-divider" />
|
||||
<button
|
||||
@click="
|
||||
textCopy();
|
||||
@@ -464,6 +474,7 @@ const clickOutsideMenu = (event: Event) => {
|
||||
>
|
||||
텍스트 복사
|
||||
</button>
|
||||
<hr class="menu-divider" />
|
||||
<button
|
||||
@click="
|
||||
saveTemplate();
|
||||
@@ -480,6 +491,7 @@ const clickOutsideMenu = (event: Event) => {
|
||||
>
|
||||
반복하기
|
||||
</button>
|
||||
<hr class="menu-divider" />
|
||||
<button
|
||||
@click="
|
||||
clearSelection();
|
||||
@@ -506,7 +518,7 @@ const clickOutsideMenu = (event: Event) => {
|
||||
</button>
|
||||
</div>
|
||||
</details>
|
||||
<button type="button" class="select-command" @click="openPicker()">명령 선택 ▾</button>
|
||||
<button type="button" class="select-command" @click="togglePicker()">명령 선택 ▾</button>
|
||||
</div>
|
||||
|
||||
<div class="queue-area">
|
||||
@@ -569,7 +581,7 @@ const clickOutsideMenu = (event: Event) => {
|
||||
:key="row.index"
|
||||
type="button"
|
||||
:aria-label="`${row.index + 1}턴 명령 입력`"
|
||||
@click="openPicker(row.index)"
|
||||
@click="togglePicker(row.index)"
|
||||
>
|
||||
✎
|
||||
</button>
|
||||
@@ -737,6 +749,14 @@ const clickOutsideMenu = (event: Event) => {
|
||||
padding: 5px 8px;
|
||||
color: #bbb;
|
||||
}
|
||||
.menu-divider {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
margin: 4px 0;
|
||||
border: 0;
|
||||
border-top: 1px solid #444;
|
||||
opacity: 1;
|
||||
}
|
||||
.step-buttons,
|
||||
.template-row {
|
||||
display: flex;
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import SkeletonLines from '../ui/SkeletonLines.vue';
|
||||
import LegacyProgressBar from '../ui/LegacyProgressBar.vue';
|
||||
import { formatSeoulHourMinute } from '../../utils/legacyDateTime';
|
||||
import { legacyExperiencePercent, ratioPercent } from '../../utils/legacyProgress';
|
||||
import { DEFAULT_GENERAL_ICON_URL, resolveGeneralIconBackgroundImage } from '../../utils/generalIcon';
|
||||
import { configuredGameAssetUrl } from '../../utils/imageAssets';
|
||||
|
||||
interface GeneralStats {
|
||||
leadership: number;
|
||||
@@ -19,9 +22,18 @@ interface GeneralProgression {
|
||||
statUpgradeLimit?: number;
|
||||
}
|
||||
|
||||
interface ItemDisplayNames {
|
||||
horse?: string | null;
|
||||
weapon?: string | null;
|
||||
book?: string | null;
|
||||
item?: string | null;
|
||||
}
|
||||
|
||||
interface GeneralInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
picture?: string | null;
|
||||
imageServer?: number | null;
|
||||
npcState: number;
|
||||
officerLevel: number;
|
||||
officerLevelText: string;
|
||||
@@ -35,17 +47,36 @@ interface GeneralInfo {
|
||||
experience: number;
|
||||
dedication: number;
|
||||
age?: number;
|
||||
turnTime?: string;
|
||||
turnTime?: string | null;
|
||||
troopId?: number;
|
||||
crewTypeId?: number;
|
||||
crewTypeName?: string;
|
||||
traits?: { personal: string; specialWar: string; specialDomestic: string };
|
||||
progression?: GeneralProgression;
|
||||
itemNames?: ItemDisplayNames;
|
||||
equipmentNames?: ItemDisplayNames;
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
general: GeneralInfo | null;
|
||||
loading: boolean;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
general: GeneralInfo | null;
|
||||
loading: boolean;
|
||||
nationColor?: string | null;
|
||||
defenceText?: string | null;
|
||||
killTurn?: number | null;
|
||||
remainingMinutes?: number | null;
|
||||
troopText?: string | null;
|
||||
penaltyText?: string | number | null;
|
||||
}>(),
|
||||
{
|
||||
nationColor: '#173d27',
|
||||
defenceText: null,
|
||||
killTurn: null,
|
||||
remainingMinutes: null,
|
||||
troopText: null,
|
||||
penaltyText: null,
|
||||
}
|
||||
);
|
||||
|
||||
const statRows = computed(() => {
|
||||
const general = props.general;
|
||||
@@ -72,137 +103,320 @@ const statRows = computed(() => {
|
||||
const experiencePercent = computed(() =>
|
||||
legacyExperiencePercent(props.general?.experience ?? 0, props.general?.progression?.experienceLevel ?? 0)
|
||||
);
|
||||
|
||||
const itemNames = computed<ItemDisplayNames>(() => props.general?.itemNames ?? props.general?.equipmentNames ?? {});
|
||||
|
||||
const generalIconBackground = computed(() => resolveGeneralIconBackgroundImage(props.general ?? {}));
|
||||
|
||||
const crewTypeIconBackground = computed(() => {
|
||||
const crewTypeId = props.general?.crewTypeId;
|
||||
if (crewTypeId === undefined || !Number.isFinite(crewTypeId)) {
|
||||
return `url(${JSON.stringify(DEFAULT_GENERAL_ICON_URL)})`;
|
||||
}
|
||||
const crewTypeUrl = `${configuredGameAssetUrl()}/crewtype${Math.trunc(crewTypeId)}.png`;
|
||||
return `url(${JSON.stringify(crewTypeUrl)}), url(${JSON.stringify(DEFAULT_GENERAL_ICON_URL)})`;
|
||||
});
|
||||
|
||||
const injuryInfo = computed(() => {
|
||||
const injury = props.general?.injury ?? 0;
|
||||
if (injury > 60) return { text: '위독', color: '#ff4d4f' };
|
||||
if (injury > 40) return { text: '심각', color: '#ff00ff' };
|
||||
if (injury > 20) return { text: '중상', color: '#ff9f1a' };
|
||||
if (injury > 0) return { text: '경상', color: '#ffff00' };
|
||||
return { text: '건강', color: '#ffffff' };
|
||||
});
|
||||
|
||||
const isBrightColor = (color: string): boolean => {
|
||||
const normalized = /^#[0-9a-f]{6}$/iu.test(color) ? color.slice(1) : '173d27';
|
||||
const red = Number.parseInt(normalized.slice(0, 2), 16);
|
||||
const green = Number.parseInt(normalized.slice(2, 4), 16);
|
||||
const blue = Number.parseInt(normalized.slice(4, 6), 16);
|
||||
return (red * 299 + green * 587 + blue * 114) / 1000 >= 150;
|
||||
};
|
||||
|
||||
const titleStyle = computed(() => {
|
||||
const backgroundColor = props.nationColor || '#173d27';
|
||||
return {
|
||||
backgroundColor,
|
||||
color: isBrightColor(backgroundColor) ? '#000000' : '#ffffff',
|
||||
};
|
||||
});
|
||||
|
||||
const ageColor = computed(() => {
|
||||
const age = props.general?.age;
|
||||
if (age === undefined) return '#ffffff';
|
||||
if (age < 53) return '#32cd32';
|
||||
if (age < 70) return '#ffff00';
|
||||
return '#ff4d4f';
|
||||
});
|
||||
|
||||
const displayTroop = computed(() => props.troopText ?? (props.general?.troopId ? String(props.general.troopId) : '-'));
|
||||
const displayPenalty = computed(() => {
|
||||
const penalty = props.penaltyText ?? '-';
|
||||
const dedication = props.general?.progression?.dedicationText ?? '무품관';
|
||||
return `${penalty} · 계급 ${dedication}`;
|
||||
});
|
||||
const displayDefence = computed(() => props.defenceText ?? '-');
|
||||
const specialText = computed(() => {
|
||||
const traits = props.general?.traits;
|
||||
return traits ? `${traits.specialDomestic || '-'} / ${traits.specialWar || '-'}` : '-';
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="general-card">
|
||||
<div v-if="props.loading">
|
||||
<div class="general-card" data-general-basic-card>
|
||||
<div v-if="props.loading" class="general-loading">
|
||||
<SkeletonLines :lines="5" />
|
||||
</div>
|
||||
<div v-else-if="!props.general" class="empty">장수 정보를 불러오지 못했습니다.</div>
|
||||
<div v-else class="general-body">
|
||||
<div class="general-title">
|
||||
{{ props.general.name }} · {{ props.general.officerLevelText }} · {{ props.general.age ?? '-' }}세 ·
|
||||
다음 턴
|
||||
{{ props.general.turnTime ? formatSeoulHourMinute(props.general.turnTime) : '-' }}
|
||||
</div>
|
||||
<template v-else>
|
||||
<div class="general-basic-grid general-body">
|
||||
<span
|
||||
class="general-image general-icon"
|
||||
role="img"
|
||||
:aria-label="`${props.general.name} 초상`"
|
||||
:style="{ backgroundImage: generalIconBackground }"
|
||||
/>
|
||||
<div class="general-title battle-general-name" :style="titleStyle">
|
||||
{{ props.general.name }} 【 {{ props.general.officerLevelText }} |
|
||||
<span :style="{ color: injuryInfo.color }">{{ injuryInfo.text }}</span> 】 다음 턴
|
||||
{{ props.general.turnTime ? formatSeoulHourMinute(props.general.turnTime) : '-' }}
|
||||
</div>
|
||||
|
||||
<div class="stat-progress-grid">
|
||||
<template v-for="stat of statRows" :key="stat.key">
|
||||
<span class="cell-label">{{ stat.label }}</span>
|
||||
<strong>{{ stat.value }}</strong>
|
||||
<div class="bar-cell" :data-stat-progress="stat.key">
|
||||
<LegacyProgressBar
|
||||
:percent="stat.percent"
|
||||
:label="`${stat.label} 성장 ${stat.accumulated} / ${stat.limit}`"
|
||||
/>
|
||||
</div>
|
||||
<strong class="stat-value">
|
||||
<span>{{ stat.value }}</span>
|
||||
<span class="bar-cell" :data-stat-progress="stat.key">
|
||||
<LegacyProgressBar
|
||||
:percent="stat.percent"
|
||||
:label="`${stat.label} 성장 ${stat.accumulated} / ${stat.limit}`"
|
||||
/>
|
||||
</span>
|
||||
</strong>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div class="legacy-grid">
|
||||
<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>{{ props.general.crewTypeName ?? '-' }}</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.progression?.dedicationText ?? '무품관' }}</strong> <span>공헌</span
|
||||
><strong>{{ props.general.dedication.toLocaleString() }}</strong>
|
||||
</div>
|
||||
<span class="cell-label">명마</span><strong>{{ itemNames.horse ?? '-' }}</strong>
|
||||
<span class="cell-label">무기</span><strong>{{ itemNames.weapon ?? '-' }}</strong>
|
||||
<span class="cell-label">서적</span><strong>{{ itemNames.book ?? '-' }}</strong>
|
||||
|
||||
<div class="experience-row">
|
||||
<span class="cell-label">Lv</span>
|
||||
<strong>{{ props.general.progression?.experienceLevel ?? 0 }}</strong>
|
||||
<div class="bar-cell" data-experience-progress>
|
||||
<span
|
||||
class="general-image general-crew-type-icon"
|
||||
role="img"
|
||||
:aria-label="`${props.general.crewTypeName ?? '병종'} 이미지`"
|
||||
:style="{ backgroundImage: crewTypeIconBackground }"
|
||||
/>
|
||||
<span class="cell-label">자금</span><strong>{{ props.general.gold.toLocaleString('ko-KR') }}</strong>
|
||||
<span class="cell-label">군량</span><strong>{{ props.general.rice.toLocaleString('ko-KR') }}</strong>
|
||||
<span class="cell-label">도구</span><strong>{{ itemNames.item ?? '-' }}</strong>
|
||||
|
||||
<span class="cell-label">병종</span><strong>{{ props.general.crewTypeName ?? '-' }}</strong>
|
||||
<span class="cell-label">병사</span><strong>{{ props.general.crew.toLocaleString('ko-KR') }}</strong>
|
||||
<span class="cell-label">성격</span><strong>{{ props.general.traits?.personal ?? '-' }}</strong>
|
||||
|
||||
<span class="cell-label">훈련</span><strong>{{ props.general.train }}</strong>
|
||||
<span class="cell-label">사기</span><strong>{{ props.general.atmos }}</strong>
|
||||
<span class="cell-label">특기</span><strong :title="specialText">{{ specialText }}</strong>
|
||||
|
||||
<span class="cell-label level-label">Lv</span>
|
||||
<strong class="level-value">{{ props.general.progression?.experienceLevel ?? 0 }}</strong>
|
||||
<span class="experience-bar" data-experience-progress>
|
||||
<LegacyProgressBar
|
||||
:percent="experiencePercent"
|
||||
:label="`경험 레벨 진행 ${experiencePercent.toFixed(1)}%`"
|
||||
/>
|
||||
</div>
|
||||
<span class="experience-total">명성 {{ props.general.experience.toLocaleString() }}</span>
|
||||
</span>
|
||||
<span class="cell-label age-label">연령</span>
|
||||
<strong class="age-value" :style="{ color: ageColor }">{{ props.general.age ?? '-' }}세</strong>
|
||||
|
||||
<span class="cell-label defence-label">수비</span>
|
||||
<strong class="defence-value">{{ displayDefence }}</strong>
|
||||
<span class="cell-label kill-label">삭턴</span>
|
||||
<strong class="kill-value">{{ props.killTurn === null ? '-' : `${props.killTurn} 턴` }}</strong>
|
||||
<span class="cell-label execute-label">실행</span>
|
||||
<strong class="execute-value">{{
|
||||
props.remainingMinutes === null ? '-' : `${props.remainingMinutes}분 남음`
|
||||
}}</strong>
|
||||
|
||||
<span class="cell-label troop-label">부대</span>
|
||||
<strong class="troop-value">{{ displayTroop }}</strong>
|
||||
<span class="cell-label penalty-label">벌점</span>
|
||||
<strong class="penalty-value">{{ displayPenalty }}</strong>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="details" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.general-title {
|
||||
.general-card {
|
||||
box-sizing: border-box;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
padding: 1px 6px;
|
||||
border-bottom: 1px solid #777;
|
||||
background: #173d27;
|
||||
text-align: center;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.stat-progress-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(30px, 1fr) minmax(34px, 1fr) 45px);
|
||||
grid-auto-rows: 21px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.stat-progress-grid > *,
|
||||
.legacy-grid > * {
|
||||
box-sizing: border-box;
|
||||
height: 21px;
|
||||
min-height: 0;
|
||||
border-right: 1px solid #666;
|
||||
border-bottom: 1px solid #666;
|
||||
padding: 2px 4px;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
background-color: #172a52;
|
||||
background-image: var(--sammo-texture-blue);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.general-basic-grid {
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
grid-template-columns: 64px repeat(3, minmax(30px, 2fr) minmax(60px, 5fr));
|
||||
grid-template-rows: repeat(9, calc(64px / 3));
|
||||
border-right: 1px solid #777;
|
||||
border-bottom: 1px solid #777;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.general-basic-grid > * {
|
||||
box-sizing: border-box;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
border-top: 1px solid #777;
|
||||
border-left: 1px solid #777;
|
||||
padding: 1px 3px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.cell-label,
|
||||
.legacy-grid > span {
|
||||
background: rgb(20 75 42 / 70%);
|
||||
.general-basic-grid > strong {
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stat-progress-grid > strong,
|
||||
.legacy-grid > strong {
|
||||
text-align: right;
|
||||
font-weight: 400;
|
||||
.cell-label {
|
||||
background-color: rgb(20 75 42 / 70%);
|
||||
}
|
||||
|
||||
.bar-cell {
|
||||
.general-image {
|
||||
display: block;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
padding: 0;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
-webkit-user-drag: none;
|
||||
}
|
||||
|
||||
.general-icon {
|
||||
grid-column: 1;
|
||||
grid-row: 1 / 4;
|
||||
}
|
||||
|
||||
.general-title {
|
||||
grid-column: 2 / 8;
|
||||
grid-row: 1;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(22px, auto) minmax(26px, 1fr);
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.bar-cell,
|
||||
.experience-bar {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
padding: 0 1px;
|
||||
}
|
||||
|
||||
.legacy-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(6, minmax(0, 1fr));
|
||||
grid-auto-rows: 21px;
|
||||
font-size: 12px;
|
||||
.general-crew-type-icon {
|
||||
grid-column: 1;
|
||||
grid-row: 4 / 7;
|
||||
}
|
||||
|
||||
.experience-row {
|
||||
display: grid;
|
||||
box-sizing: border-box;
|
||||
grid-template-columns: 32px 38px minmax(120px, 1fr) 112px;
|
||||
height: 20px;
|
||||
min-height: 20px;
|
||||
border-bottom: 1px solid #666;
|
||||
font-size: 12px;
|
||||
.level-label {
|
||||
grid-column: 1;
|
||||
grid-row: 7;
|
||||
}
|
||||
|
||||
.experience-row > * {
|
||||
display: grid;
|
||||
align-content: center;
|
||||
box-sizing: border-box;
|
||||
border-right: 1px solid #666;
|
||||
padding: 1px 4px;
|
||||
text-align: center;
|
||||
.level-value {
|
||||
grid-column: 2;
|
||||
grid-row: 7;
|
||||
}
|
||||
|
||||
.experience-bar {
|
||||
grid-column: 3 / 6;
|
||||
grid-row: 7;
|
||||
}
|
||||
|
||||
.age-label {
|
||||
grid-column: 6;
|
||||
grid-row: 7;
|
||||
}
|
||||
|
||||
.age-value {
|
||||
grid-column: 7;
|
||||
grid-row: 7;
|
||||
}
|
||||
|
||||
.defence-label {
|
||||
grid-column: 1;
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
.defence-value {
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
.kill-label {
|
||||
grid-column: 4;
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
.kill-value {
|
||||
grid-column: 5;
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
.execute-label {
|
||||
grid-column: 6;
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
.execute-value {
|
||||
grid-column: 7;
|
||||
grid-row: 8;
|
||||
}
|
||||
|
||||
.troop-label {
|
||||
grid-column: 1;
|
||||
grid-row: 9;
|
||||
}
|
||||
|
||||
.troop-value {
|
||||
grid-column: 2 / 4;
|
||||
grid-row: 9;
|
||||
}
|
||||
|
||||
.penalty-label {
|
||||
grid-column: 4;
|
||||
grid-row: 9;
|
||||
}
|
||||
|
||||
.penalty-value {
|
||||
grid-column: 5 / 8;
|
||||
grid-row: 9;
|
||||
}
|
||||
|
||||
.general-loading,
|
||||
.empty {
|
||||
min-height: 192px;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.empty {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
import { computed } from 'vue';
|
||||
import { resolveTournamentStageName } from '../../utils/tournamentStatus';
|
||||
|
||||
const props = defineProps<{
|
||||
tournamentStage: number;
|
||||
status: {
|
||||
onlineUserCount: number;
|
||||
onlineNations: string;
|
||||
@@ -13,15 +17,24 @@ defineProps<{
|
||||
} | null;
|
||||
} | null;
|
||||
}>();
|
||||
|
||||
const tournamentStatus = computed(() => resolveTournamentStageName(props.tournamentStage));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="front-status" aria-label="접속 현황과 국가 방침">
|
||||
<div class="status-row vote-status">
|
||||
<RouterLink v-if="status?.latestVote" to="/survey">
|
||||
<span class="vote-label">설문 진행 중: </span>{{ status.latestVote.title }}
|
||||
</RouterLink>
|
||||
<span v-else class="vote-empty">진행중인 설문 없음</span>
|
||||
<div class="activity-status" aria-label="설문과 토너먼트 진행 현황">
|
||||
<div class="status-row tournament-status">
|
||||
<RouterLink to="/tournament">
|
||||
<span class="tournament-label">토너먼트: </span>{{ tournamentStatus }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="status-row vote-status">
|
||||
<RouterLink v-if="status?.latestVote" to="/survey">
|
||||
<span class="vote-label">설문: </span>{{ status.latestVote.title }}
|
||||
</RouterLink>
|
||||
<span v-else class="vote-empty">설문: 진행 중인 설문 없음</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="status-row online-nations">접속중인 국가: {{ status?.onlineNations ?? '' }}</div>
|
||||
<div class="status-row online-users">【 접속자 】 {{ status?.onlineGenerals ?? '' }}</div>
|
||||
@@ -71,19 +84,28 @@ defineProps<{
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.vote-status {
|
||||
width: 33.333333%;
|
||||
.activity-status {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
width: 66.666667%;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.activity-status .status-row {
|
||||
padding-right: 0;
|
||||
padding-left: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.vote-status a {
|
||||
.activity-status a {
|
||||
color: #fff;
|
||||
text-decoration: gray underline;
|
||||
}
|
||||
|
||||
.tournament-label {
|
||||
color: #ffc107;
|
||||
}
|
||||
|
||||
.vote-label {
|
||||
color: cyan;
|
||||
}
|
||||
@@ -93,8 +115,8 @@ defineProps<{
|
||||
}
|
||||
|
||||
@media (max-width: 991px) {
|
||||
.vote-status {
|
||||
width: 50%;
|
||||
.activity-status {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import GeneralIdentity from '../ui/GeneralIdentity.vue';
|
||||
import {
|
||||
buildTournamentBracket,
|
||||
type TournamentBracketMatch,
|
||||
@@ -89,7 +90,12 @@ const odds = (id: number | null) => {
|
||||
:class="{ advanced: bracket.champion.advanced }"
|
||||
:data-general-id="bracket.champion.id ?? undefined"
|
||||
>
|
||||
{{ bracket.champion.name }}
|
||||
<GeneralIdentity
|
||||
:name="bracket.champion.name"
|
||||
:picture="bracket.champion.picture"
|
||||
:image-server="bracket.champion.imageServer"
|
||||
:icon-size="24"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -110,7 +116,12 @@ const odds = (id: number | null) => {
|
||||
:class="{ advanced: slot.advanced }"
|
||||
:data-general-id="slot.id ?? undefined"
|
||||
>
|
||||
{{ slot.name }}
|
||||
<GeneralIdentity
|
||||
:name="slot.name"
|
||||
:picture="slot.picture"
|
||||
:image-server="slot.imageServer"
|
||||
:icon-size="22"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="connector-row" :style="{ '--connector-count': round.slots.length }">
|
||||
@@ -140,7 +151,12 @@ const odds = (id: number | null) => {
|
||||
:class="{ advanced: slot.advanced }"
|
||||
:data-general-id="slot.id ?? undefined"
|
||||
>
|
||||
{{ slot.name }}
|
||||
<GeneralIdentity
|
||||
:name="slot.name"
|
||||
:picture="slot.picture"
|
||||
:image-server="slot.imageServer"
|
||||
:icon-size="20"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="bracket-round bracket-odds" :style="roundStyle(bracket.top16)">
|
||||
@@ -183,9 +199,17 @@ const odds = (id: number | null) => {
|
||||
:key="`mobile-${columnIndex}-${slot.id ?? 'empty'}-${slotIndex}`"
|
||||
class="mobile-bracket-name"
|
||||
:class="{ advanced: slot.advanced }"
|
||||
:style="{ left: `${mobileX[columnIndex]}px`, top: `${mobileY(columnIndex, slotIndex)}px` }"
|
||||
:style="{
|
||||
left: `${(mobileX[columnIndex]! / 390) * 100}%`,
|
||||
top: `${mobileY(columnIndex, slotIndex)}px`,
|
||||
}"
|
||||
>
|
||||
{{ slot.name }}
|
||||
<GeneralIdentity
|
||||
:name="slot.name"
|
||||
:picture="slot.picture"
|
||||
:image-server="slot.imageServer"
|
||||
:icon-size="18"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
@@ -210,21 +234,23 @@ const odds = (id: number | null) => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.bracket-canvas {
|
||||
width: 2000px;
|
||||
min-width: 2000px;
|
||||
width: 100%;
|
||||
min-width: 1000px;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.mobile-bracket {
|
||||
position: relative;
|
||||
display: none;
|
||||
width: 390px;
|
||||
width: 100%;
|
||||
max-width: 390px;
|
||||
height: 544px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.mobile-bracket svg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 390px;
|
||||
width: 100%;
|
||||
height: 544px;
|
||||
}
|
||||
.mobile-connector {
|
||||
@@ -239,14 +265,16 @@ const odds = (id: number | null) => {
|
||||
.mobile-bracket-name {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
width: 64px;
|
||||
width: clamp(58px, 18vw, 72px);
|
||||
overflow: hidden;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid #555;
|
||||
background: rgb(58 33 24 / 92%);
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
min-height: 26px;
|
||||
padding: 2px;
|
||||
font-size: 11px;
|
||||
line-height: 20px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -265,7 +293,7 @@ const odds = (id: number | null) => {
|
||||
}
|
||||
.bracket-name {
|
||||
overflow: hidden;
|
||||
padding: 0 3px;
|
||||
padding: 2px 3px;
|
||||
color: #fff;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
@@ -321,8 +349,8 @@ const odds = (id: number | null) => {
|
||||
}
|
||||
@media (max-width: 800px) {
|
||||
.tournament-bracket {
|
||||
width: 100vw;
|
||||
max-width: 100vw;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.bracket-canvas {
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { resolveGeneralIconUrl, useDefaultGeneralIcon, type GeneralIconSource } from '../../utils/generalIcon';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
name: string;
|
||||
picture?: GeneralIconSource['picture'];
|
||||
imageServer?: GeneralIconSource['imageServer'];
|
||||
iconSize?: number;
|
||||
hideIcon?: boolean;
|
||||
}>(),
|
||||
{
|
||||
picture: null,
|
||||
imageServer: 0,
|
||||
iconSize: 28,
|
||||
hideIcon: false,
|
||||
}
|
||||
);
|
||||
|
||||
const iconUrl = computed(() =>
|
||||
resolveGeneralIconUrl({
|
||||
picture: props.picture,
|
||||
imageServer: props.imageServer,
|
||||
})
|
||||
);
|
||||
const identityStyle = computed(() => ({ '--general-identity-icon-size': `${props.iconSize}px` }));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="general-identity" :style="identityStyle">
|
||||
<img
|
||||
v-if="!hideIcon && name !== '-'"
|
||||
class="general-identity-icon"
|
||||
:src="iconUrl"
|
||||
alt=""
|
||||
aria-hidden="true"
|
||||
@error="useDefaultGeneralIcon"
|
||||
/>
|
||||
<span class="general-identity-name">{{ name }}</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.general-identity {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.general-identity-icon {
|
||||
width: var(--general-identity-icon-size);
|
||||
height: var(--general-identity-icon-size);
|
||||
flex: 0 0 var(--general-identity-icon-size);
|
||||
border: 1px solid rgb(255 255 255 / 28%);
|
||||
background: #111;
|
||||
object-fit: cover;
|
||||
}
|
||||
.general-identity-name {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
@@ -15,7 +15,9 @@ type GeneralProgress = {
|
||||
};
|
||||
};
|
||||
|
||||
const props = defineProps<{ general: GeneralProgress }>();
|
||||
const props = withDefaults(defineProps<{ general: GeneralProgress; showPrimary?: boolean }>(), {
|
||||
showPrimary: true,
|
||||
});
|
||||
|
||||
const statRows = computed(() =>
|
||||
[
|
||||
@@ -50,7 +52,7 @@ const experiencePercent = computed(() =>
|
||||
|
||||
<template>
|
||||
<div class="legacy-general-progress">
|
||||
<div class="stat-grid">
|
||||
<div v-if="props.showPrimary" class="stat-grid">
|
||||
<template v-for="stat of statRows" :key="stat.key">
|
||||
<span class="cell-label">{{ stat.label }}</span>
|
||||
<strong>{{ stat.value }}</strong>
|
||||
@@ -60,7 +62,7 @@ const experiencePercent = computed(() =>
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div class="experience-row">
|
||||
<div v-if="props.showPrimary" class="experience-row">
|
||||
<span class="cell-label">Lv</span>
|
||||
<strong>{{ props.general.progression.experienceLevel }}</strong>
|
||||
<LegacyProgressBar
|
||||
|
||||
Reference in New Issue
Block a user