feat: 사령턴 대상 선택 정보를 보강
도시·수도 지도와 명령별 국가/장수 상세 목록을 추가한다.\nRef 금액 프리셋과 현재 조건 기반 우선 정렬을 예약 입력에 연결한다.
This commit is contained in:
@@ -251,7 +251,8 @@ const selectCommand = (commandKey: string) => {
|
||||
selectedCommand.value = command;
|
||||
commandArgs.value = {};
|
||||
commandArgsValid.value = !command.reqArg;
|
||||
if (!command.reqArg) submitCommand();
|
||||
const needsInformationalConfirmation = commandArgumentPresentation(command.key).mapTarget === 'capital';
|
||||
if (!command.reqArg && !needsInformationalConfirmation) submitCommand();
|
||||
};
|
||||
const submitCommand = () => {
|
||||
const command = selectedCommand.value;
|
||||
@@ -737,7 +738,11 @@ const clickOutsideMenu = (event: Event) => {
|
||||
@submit="submitCommand"
|
||||
/>
|
||||
<CommandArgumentForm
|
||||
v-else-if="selectedCommand.reqArg && props.commandTable"
|
||||
v-else-if="
|
||||
props.commandTable &&
|
||||
(selectedCommand.reqArg ||
|
||||
commandArgumentPresentation(selectedCommand.key).mapTarget === 'capital')
|
||||
"
|
||||
:command-key="selectedCommand.key"
|
||||
:fields="selectedCommand.inputFields"
|
||||
:options="props.commandTable.inputOptions"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
export type CommandArgumentPresentation = {
|
||||
lines: string[];
|
||||
mapTarget?: 'city' | 'nation';
|
||||
mapTarget?: 'city' | 'nation' | 'capital';
|
||||
};
|
||||
|
||||
const cityTarget = (lines: string[]): CommandArgumentPresentation => ({ lines, mapTarget: 'city' });
|
||||
const nationTarget = (lines: string[]): CommandArgumentPresentation => ({ lines, mapTarget: 'nation' });
|
||||
const capitalTarget = (lines: string[]): CommandArgumentPresentation => ({ lines, mapTarget: 'capital' });
|
||||
|
||||
// Ref hwe/ts/processing의 명령별 안내를 예약 명령 옵션창에 맞게 옮긴다.
|
||||
// 징병/모병은 별도 이관 범위이므로 이 표에 넣지 않는다.
|
||||
@@ -33,6 +34,14 @@ const PRESENTATIONS: Record<string, CommandArgumentPresentation> = {
|
||||
]),
|
||||
cr_인구이동: cityTarget(['현재 도시의 인구를 선택한 인접 도시로 이동합니다.']),
|
||||
che_발령: cityTarget(['선택한 도시로 아국 장수를 발령합니다.', '아국 도시만 대상이 됩니다.']),
|
||||
che_증축: capitalTarget([
|
||||
'현재 수도를 증축해 인구·내정·성벽의 최대치를 높입니다.',
|
||||
'지도에는 이번 명령의 대상인 현재 수도가 강조됩니다.',
|
||||
]),
|
||||
che_감축: capitalTarget([
|
||||
'현재 수도를 감축해 인구·내정·성벽의 최대치를 낮추고 국고를 회수합니다.',
|
||||
'지도에는 이번 명령의 대상인 현재 수도가 강조됩니다.',
|
||||
]),
|
||||
|
||||
che_선전포고: nationTarget([
|
||||
'선택한 국가에 선전포고합니다.',
|
||||
|
||||
@@ -3,6 +3,19 @@ export type CommandOption = {
|
||||
label: string;
|
||||
color?: string;
|
||||
description?: string;
|
||||
availableNow?: boolean;
|
||||
gold?: number;
|
||||
rice?: number;
|
||||
crew?: number;
|
||||
troopId?: number;
|
||||
};
|
||||
|
||||
export type CommandAmountPreset = {
|
||||
values: number[];
|
||||
defaultValue: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step: number;
|
||||
};
|
||||
|
||||
export type CommandMapData = {
|
||||
@@ -94,6 +107,7 @@ export type CommandTable = {
|
||||
inputOptions: {
|
||||
cities: CommandOption[];
|
||||
nations: CommandOption[];
|
||||
nationTargets?: Record<string, CommandOption[]>;
|
||||
generals: CommandOption[];
|
||||
generalTargets?: Record<string, CommandOption[]>;
|
||||
crewTypes: CommandOption[];
|
||||
@@ -102,6 +116,7 @@ export type CommandTable = {
|
||||
colors: CommandOption[];
|
||||
items: Record<string, CommandOption[]>;
|
||||
recruitment: RecruitmentInfo | null;
|
||||
amountPresets?: Record<string, CommandAmountPreset>;
|
||||
context?: CommandInputContext;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -30,11 +30,31 @@ const values = reactive<Record<string, unknown>>({});
|
||||
const presentation = computed(() => commandArgumentPresentation(props.commandKey));
|
||||
const visibleFields = computed(() => props.fields.filter((entry) => entry.kind !== 'hidden'));
|
||||
|
||||
const amountPreset = computed(() => props.options.amountPresets?.[props.commandKey]);
|
||||
|
||||
const sortGeneralOptions = (options: CommandOption[]): CommandOption[] => {
|
||||
const result = [...options];
|
||||
const resourceKey = values.isGold === false ? 'rice' : 'gold';
|
||||
if (props.commandKey === 'che_포상') {
|
||||
return result.sort((left, right) => (left[resourceKey] ?? 0) - (right[resourceKey] ?? 0));
|
||||
}
|
||||
if (props.commandKey === 'che_몰수') {
|
||||
return result.sort((left, right) => (right[resourceKey] ?? 0) - (left[resourceKey] ?? 0));
|
||||
}
|
||||
if (props.commandKey === 'che_부대탈퇴지시') {
|
||||
return result.sort((left, right) => Number(right.availableNow) - Number(left.availableNow));
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
const optionsFor = (field: CommandInputField): CommandOption[] => {
|
||||
if (field.options) return field.options;
|
||||
if (!field.optionSource) return [];
|
||||
if (field.optionSource === 'generals') {
|
||||
return props.options.generalTargets?.[props.commandKey] ?? props.options.generals;
|
||||
return sortGeneralOptions(props.options.generalTargets?.[props.commandKey] ?? props.options.generals);
|
||||
}
|
||||
if (field.optionSource === 'nations') {
|
||||
return props.options.nationTargets?.[props.commandKey] ?? props.options.nations;
|
||||
}
|
||||
if (field.optionSource === 'items') {
|
||||
return props.options.items[String(values.itemType ?? '')] ?? [];
|
||||
@@ -45,10 +65,16 @@ const optionsFor = (field: CommandInputField): CommandOption[] => {
|
||||
const defaultValue = (field: CommandInputField): unknown => {
|
||||
if (field.kind === 'hidden') return field.constValue;
|
||||
if (field.kind === 'boolean') return true;
|
||||
if (field.kind === 'numberTuple') return [field.min ?? 0, field.min ?? 0];
|
||||
if (field.kind === 'number') return field.min ?? 0;
|
||||
if (field.kind === 'numberTuple') {
|
||||
const value = amountPreset.value?.defaultValue ?? field.min ?? 0;
|
||||
return [value, value];
|
||||
}
|
||||
if (field.kind === 'number') return amountPreset.value?.defaultValue ?? field.min ?? 0;
|
||||
if (field.kind === 'select') {
|
||||
const options = optionsFor(field);
|
||||
const commandSpecificNationTargets =
|
||||
field.optionSource === 'nations' ? props.options.nationTargets?.[props.commandKey] : undefined;
|
||||
if (commandSpecificNationTargets?.length) return commandSpecificNationTargets[0]?.value ?? '';
|
||||
const mapDefault =
|
||||
field.optionSource === 'cities' && (field.key === 'destCityId' || field.key === 'destCityID')
|
||||
? props.mapData?.myCity
|
||||
@@ -96,7 +122,8 @@ const showMap = computed(
|
||||
() =>
|
||||
Boolean(props.mapData && props.mapLayout) &&
|
||||
((presentation.value.mapTarget === 'city' && cityTargetField.value) ||
|
||||
(presentation.value.mapTarget === 'nation' && nationTargetField.value))
|
||||
(presentation.value.mapTarget === 'nation' && nationTargetField.value) ||
|
||||
presentation.value.mapTarget === 'capital')
|
||||
);
|
||||
const mapSelectedCityId = computed<number | null>(() => {
|
||||
if (!props.mapData) return null;
|
||||
@@ -107,7 +134,15 @@ const mapSelectedCityId = computed<number | null>(() => {
|
||||
if (presentation.value.mapTarget === 'nation' && nationTargetField.value) {
|
||||
const value = values[nationTargetField.value.key];
|
||||
if (typeof value !== 'number') return null;
|
||||
return props.mapData.cityList.find((entry) => entry[3] === value)?.[0] ?? null;
|
||||
return (
|
||||
props.mapData.nationList.find((entry) => entry[0] === value)?.[3] ??
|
||||
props.mapData.cityList.find((entry) => entry[3] === value)?.[0] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
if (presentation.value.mapTarget === 'capital') {
|
||||
const myNation = props.mapData.myNation;
|
||||
return props.mapData.nationList.find((entry) => entry[0] === myNation)?.[3] ?? null;
|
||||
}
|
||||
return null;
|
||||
});
|
||||
@@ -129,6 +164,11 @@ const selectedMapTargetName = computed(() => {
|
||||
if (typeof nationId !== 'number') return '-';
|
||||
return props.mapData?.nationList.find((nation) => nation[0] === nationId)?.[1] ?? '-';
|
||||
}
|
||||
if (presentation.value.mapTarget === 'capital') {
|
||||
const cityId = mapSelectedCityId.value;
|
||||
if (!cityId) return '-';
|
||||
return props.mapLayout?.cityList.find((city) => city.id === cityId)?.name ?? '-';
|
||||
}
|
||||
return '-';
|
||||
});
|
||||
|
||||
@@ -181,6 +221,14 @@ const mapTargetSummary = computed(() => {
|
||||
const cityCount = props.mapData.cityList.filter((entry) => entry[3] === value).length;
|
||||
return `${nation[1]} · 수도 ${capital?.name ?? '-'} · 도시 ${cityCount.toLocaleString()}개`;
|
||||
}
|
||||
if (presentation.value.mapTarget === 'capital' && mapSelectedCityId.value) {
|
||||
const city = props.mapLayout.cityList.find((entry) => entry.id === mapSelectedCityId.value);
|
||||
const dynamic = props.mapData.cityList.find((entry) => entry[0] === mapSelectedCityId.value);
|
||||
if (!city) return '';
|
||||
return `${city.name} · ${props.mapLayout.regionMap[dynamic?.[4] ?? city.region]} · ${
|
||||
props.mapLayout.levelMap[dynamic?.[1] ?? city.level]
|
||||
} · 현재 수도`;
|
||||
}
|
||||
return '';
|
||||
});
|
||||
|
||||
@@ -225,6 +273,35 @@ const setTupleValue = (field: CommandInputField, index: number, rawValue: string
|
||||
values[field.key] = tuple;
|
||||
};
|
||||
|
||||
const setNumberPreset = (field: CommandInputField, rawValue: string, tupleIndex?: number) => {
|
||||
if (!rawValue) return;
|
||||
if (tupleIndex === undefined) {
|
||||
values[field.key] = Number(rawValue);
|
||||
return;
|
||||
}
|
||||
setTupleValue(field, tupleIndex, rawValue);
|
||||
};
|
||||
|
||||
const effectiveMin = (field: CommandInputField): number | undefined => amountPreset.value?.min ?? field.min;
|
||||
const effectiveMax = (field: CommandInputField): number | undefined => amountPreset.value?.max ?? field.max;
|
||||
const effectiveStep = (field: CommandInputField): number | undefined => amountPreset.value?.step ?? field.step;
|
||||
|
||||
const OPTION_CARD_COMMANDS = new Set([
|
||||
'che_물자원조',
|
||||
'che_불가침제의',
|
||||
'che_선전포고',
|
||||
'che_종전제의',
|
||||
'che_불가침파기제의',
|
||||
'che_포상',
|
||||
'che_발령',
|
||||
'che_몰수',
|
||||
'che_부대탈퇴지시',
|
||||
]);
|
||||
const showOptionCards = (field: CommandInputField): boolean =>
|
||||
field.kind === 'select' &&
|
||||
Boolean(field.optionSource && ['nations', 'generals'].includes(field.optionSource)) &&
|
||||
OPTION_CARD_COMMANDS.has(props.commandKey);
|
||||
|
||||
const isValid = computed(() =>
|
||||
props.fields.every((field) => {
|
||||
const value = values[field.key];
|
||||
@@ -237,14 +314,18 @@ const isValid = computed(() =>
|
||||
);
|
||||
}
|
||||
if (field.kind === 'number') {
|
||||
const min = effectiveMin(field);
|
||||
const max = effectiveMax(field);
|
||||
return (
|
||||
typeof value === 'number' &&
|
||||
Number.isFinite(value) &&
|
||||
(field.min === undefined || value >= field.min) &&
|
||||
(field.max === undefined || value <= field.max)
|
||||
(min === undefined || value >= min) &&
|
||||
(max === undefined || value <= max)
|
||||
);
|
||||
}
|
||||
if (field.kind === 'numberTuple') {
|
||||
const min = effectiveMin(field);
|
||||
const max = effectiveMax(field);
|
||||
return (
|
||||
Array.isArray(value) &&
|
||||
value.length === 2 &&
|
||||
@@ -252,8 +333,8 @@ const isValid = computed(() =>
|
||||
(entry) =>
|
||||
typeof entry === 'number' &&
|
||||
Number.isFinite(entry) &&
|
||||
(field.min === undefined || entry >= field.min) &&
|
||||
(field.max === undefined || entry <= field.max)
|
||||
(min === undefined || entry >= min) &&
|
||||
(max === undefined || entry <= max)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -274,7 +355,11 @@ watch(
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="props.fields.length" class="command-argument-form" data-testid="command-argument-form">
|
||||
<div
|
||||
v-if="props.fields.length || showMap || presentation.lines.length"
|
||||
class="command-argument-form"
|
||||
data-testid="command-argument-form"
|
||||
>
|
||||
<div v-if="showMap" class="command-map" data-testid="command-argument-map">
|
||||
<MapViewer
|
||||
:map-data="props.mapData ?? null"
|
||||
@@ -284,18 +369,24 @@ watch(
|
||||
:detail-mode="true"
|
||||
:fit-container="true"
|
||||
:show-current-city-marker="true"
|
||||
:readonly="presentation.mapTarget === 'capital'"
|
||||
@select-city="selectMapCity"
|
||||
/>
|
||||
<small>지도에서 도시를 클릭하거나 아래 목록에서 대상을 선택하세요.</small>
|
||||
<small v-if="presentation.mapTarget === 'capital'">현재 명령이 적용될 수도를 지도에서 확인하세요.</small>
|
||||
<small v-else>지도에서 도시를 클릭하거나 아래 목록에서 대상을 선택하세요.</small>
|
||||
<div class="map-selection-status" aria-live="polite" data-testid="command-map-selection-status">
|
||||
<span class="current-city-status">
|
||||
<span v-if="presentation.mapTarget !== 'capital'" class="current-city-status">
|
||||
<span class="status-key">현재 도시</span>
|
||||
<strong>{{ currentCityName }}</strong>
|
||||
</span>
|
||||
<span aria-hidden="true">→</span>
|
||||
<span v-if="presentation.mapTarget !== 'capital'" aria-hidden="true">→</span>
|
||||
<span class="selected-target-status">
|
||||
<span class="status-key">{{
|
||||
presentation.mapTarget === 'nation' ? '선택 국가' : '선택 도시'
|
||||
presentation.mapTarget === 'nation'
|
||||
? '선택 국가'
|
||||
: presentation.mapTarget === 'capital'
|
||||
? '현재 수도'
|
||||
: '선택 도시'
|
||||
}}</span>
|
||||
<strong>{{ selectedMapTargetName }}</strong>
|
||||
</span>
|
||||
@@ -320,16 +411,29 @@ watch(
|
||||
:maxlength="field.max"
|
||||
@input="values[field.key] = ($event.target as HTMLInputElement).value"
|
||||
/>
|
||||
<input
|
||||
v-else-if="field.kind === 'number'"
|
||||
:id="`command-arg-${field.key}`"
|
||||
type="number"
|
||||
:value="Number(values[field.key] ?? 0)"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step"
|
||||
@input="values[field.key] = Number(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<div v-else-if="field.kind === 'number'" class="number-options">
|
||||
<input
|
||||
:id="`command-arg-${field.key}`"
|
||||
type="number"
|
||||
:value="Number(values[field.key] ?? 0)"
|
||||
:min="effectiveMin(field)"
|
||||
:max="effectiveMax(field)"
|
||||
:step="effectiveStep(field)"
|
||||
@input="values[field.key] = Number(($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<select
|
||||
v-if="amountPreset"
|
||||
aria-label="금액 프리셋"
|
||||
class="amount-preset"
|
||||
value=""
|
||||
@change="setNumberPreset(field, ($event.target as HTMLSelectElement).value)"
|
||||
>
|
||||
<option value="" disabled>프리셋</option>
|
||||
<option v-for="preset in amountPreset.values" :key="preset" :value="preset">
|
||||
{{ preset.toLocaleString() }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<select
|
||||
v-else-if="field.kind === 'select'"
|
||||
:id="`command-arg-${field.key}`"
|
||||
@@ -362,11 +466,23 @@ watch(
|
||||
<input
|
||||
type="number"
|
||||
:value="(values[field.key] as number[] | undefined)?.[index] ?? 0"
|
||||
:min="field.min"
|
||||
:max="field.max"
|
||||
:step="field.step"
|
||||
:min="effectiveMin(field)"
|
||||
:max="effectiveMax(field)"
|
||||
:step="effectiveStep(field)"
|
||||
@input="setTupleValue(field, index, ($event.target as HTMLInputElement).value)"
|
||||
/>
|
||||
<select
|
||||
v-if="amountPreset"
|
||||
:aria-label="`${tupleLabel} 금액 프리셋`"
|
||||
class="amount-preset"
|
||||
value=""
|
||||
@change="setNumberPreset(field, ($event.target as HTMLSelectElement).value, index)"
|
||||
>
|
||||
<option value="" disabled>프리셋</option>
|
||||
<option v-for="preset in amountPreset.values" :key="preset" :value="preset">
|
||||
{{ preset.toLocaleString() }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<div
|
||||
@@ -384,6 +500,30 @@ watch(
|
||||
/>
|
||||
<span>{{ selectedOptionFor(field)?.description }}</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="showOptionCards(field)"
|
||||
class="target-option-list"
|
||||
:data-testid="field.optionSource === 'nations' ? 'nation-target-list' : 'general-target-list'"
|
||||
>
|
||||
<button
|
||||
v-for="option in optionsFor(field)"
|
||||
:key="String(option.value)"
|
||||
type="button"
|
||||
class="target-option"
|
||||
:class="{
|
||||
selected: option.value === values[field.key],
|
||||
unavailable: option.availableNow === false,
|
||||
}"
|
||||
@click="setSelectValue(field, String(option.value))"
|
||||
>
|
||||
<span v-if="option.color" class="option-color" :style="{ backgroundColor: option.color }" />
|
||||
<strong>{{ option.label }}</strong>
|
||||
<span class="target-state">{{
|
||||
option.availableNow === false ? '현재 불가' : option.availableNow ? '우선 대상' : '대상'
|
||||
}}</span>
|
||||
<small>{{ option.description }}</small>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!isValid" class="argument-error" role="alert">필수 입력을 확인하세요.</div>
|
||||
</div>
|
||||
@@ -484,6 +624,74 @@ watch(
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.target-option-list {
|
||||
grid-column: 1 / -1;
|
||||
display: grid;
|
||||
max-height: 230px;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid rgba(201, 164, 90, 0.2);
|
||||
}
|
||||
|
||||
.target-option {
|
||||
display: grid;
|
||||
grid-template-columns: 18px minmax(0, auto) max-content;
|
||||
align-items: center;
|
||||
gap: 3px 7px;
|
||||
border: 0;
|
||||
border-bottom: 1px solid rgba(201, 164, 90, 0.16);
|
||||
padding: 6px 8px;
|
||||
background: rgba(7, 9, 12, 0.82);
|
||||
color: #e8ddc4;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.target-option:hover,
|
||||
.target-option:focus-visible {
|
||||
background: rgba(201, 164, 90, 0.13);
|
||||
outline: 1px solid rgba(201, 164, 90, 0.6);
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.target-option.selected {
|
||||
background: rgba(201, 164, 90, 0.2);
|
||||
box-shadow: inset 3px 0 #e0bc6d;
|
||||
}
|
||||
|
||||
.target-option.unavailable {
|
||||
color: rgba(232, 221, 196, 0.58);
|
||||
}
|
||||
|
||||
.target-option > strong {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.target-option > .option-color + strong {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.target-option > strong:first-child {
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
|
||||
.target-state {
|
||||
grid-column: 3;
|
||||
color: #aee6a7;
|
||||
font-size: 10px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.target-option.unavailable .target-state {
|
||||
color: #e9a29a;
|
||||
}
|
||||
|
||||
.target-option small {
|
||||
grid-column: 1 / -1;
|
||||
color: inherit;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.option-color {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
@@ -511,6 +719,19 @@ select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.number-options {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(90px, 0.45fr);
|
||||
gap: 5px;
|
||||
padding-right: 6px;
|
||||
}
|
||||
|
||||
.number-options input,
|
||||
.number-options select {
|
||||
width: 100%;
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.boolean-options,
|
||||
.tuple-options {
|
||||
display: flex;
|
||||
@@ -531,17 +752,34 @@ select {
|
||||
}
|
||||
|
||||
.tuple-options label {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(68px, 1fr) minmax(82px, 0.7fr);
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tuple-options input {
|
||||
width: 80px;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tuple-options .amount-preset {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.tuple-options {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tuple-options label {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.argument-error {
|
||||
padding: 5px 8px;
|
||||
color: #ff9a8f;
|
||||
|
||||
@@ -22,6 +22,7 @@ const props = defineProps<{
|
||||
showName: boolean;
|
||||
mapScale: number;
|
||||
selectOnly?: boolean;
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -33,18 +34,27 @@ const emit = defineEmits<{
|
||||
const size = computed(() => (6 + props.city.level * 2) * props.mapScale);
|
||||
const stateSize = computed(() => 8 * props.mapScale);
|
||||
const stateOffset = computed(() => -6 * props.mapScale);
|
||||
const selectCity = () => emit('select', props.city.id);
|
||||
const selectCity = () => {
|
||||
if (!props.readonly) emit('select', props.city.id);
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="props.selectOnly ? 'button' : RouterLink"
|
||||
:is="props.readonly ? 'div' : props.selectOnly ? 'button' : RouterLink"
|
||||
class="map-city"
|
||||
:type="props.selectOnly ? 'button' : undefined"
|
||||
:to="props.selectOnly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }"
|
||||
:type="!props.readonly && props.selectOnly ? 'button' : undefined"
|
||||
:to="
|
||||
props.selectOnly || props.readonly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }
|
||||
"
|
||||
:class="[
|
||||
`state-${props.city.stateClass}`,
|
||||
{ mine: props.city.isMyCity, selected: props.city.selected, 'supply-off': !props.city.supply },
|
||||
{
|
||||
mine: props.city.isMyCity,
|
||||
selected: props.city.selected,
|
||||
'supply-off': !props.city.supply,
|
||||
readonly: props.readonly,
|
||||
},
|
||||
]"
|
||||
:style="{ left: `${props.city.x}px`, top: `${props.city.y}px` }"
|
||||
@mouseenter="emit('hover', props.city.id)"
|
||||
@@ -86,6 +96,10 @@ const selectCity = () => emit('select', props.city.id);
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.map-city.readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.city-dot {
|
||||
border: 1px solid rgba(232, 221, 196, 0.6);
|
||||
display: flex;
|
||||
|
||||
@@ -48,6 +48,7 @@ const props = defineProps<{
|
||||
themeName: string;
|
||||
mapScale: number;
|
||||
selectOnly?: boolean;
|
||||
readonly?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@@ -143,7 +144,9 @@ const capitalIconStyle = computed(() => ({
|
||||
height: `${10 * props.mapScale}px`,
|
||||
}));
|
||||
|
||||
const selectCity = () => emit('select', props.city.id);
|
||||
const selectCity = () => {
|
||||
if (!props.readonly) emit('select', props.city.id);
|
||||
};
|
||||
|
||||
const cityStateStyle = computed(() => ({
|
||||
width: `${12 * props.mapScale}px`,
|
||||
@@ -154,11 +157,20 @@ const cityStateStyle = computed(() => ({
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="props.selectOnly ? 'button' : RouterLink"
|
||||
:is="props.readonly ? 'div' : props.selectOnly ? 'button' : RouterLink"
|
||||
class="city-base"
|
||||
:type="props.selectOnly ? 'button' : undefined"
|
||||
:to="props.selectOnly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }"
|
||||
:class="[{ mine: props.city.isMyCity, selected: props.city.selected, 'supply-off': !props.city.supply }]"
|
||||
:type="!props.readonly && props.selectOnly ? 'button' : undefined"
|
||||
:to="
|
||||
props.selectOnly || props.readonly ? undefined : { name: 'current-city', query: { cityId: props.city.id } }
|
||||
"
|
||||
:class="[
|
||||
{
|
||||
mine: props.city.isMyCity,
|
||||
selected: props.city.selected,
|
||||
'supply-off': !props.city.supply,
|
||||
readonly: props.readonly,
|
||||
},
|
||||
]"
|
||||
:style="cityBaseStyle"
|
||||
@mouseenter="emit('hover', props.city.id)"
|
||||
@mouseleave="emit('leave')"
|
||||
@@ -195,6 +207,10 @@ const cityStateStyle = computed(() => ({
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.city-base.readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.city-bg {
|
||||
position: absolute;
|
||||
background-position: center;
|
||||
|
||||
@@ -72,6 +72,7 @@ const props = withDefaults(
|
||||
detailMode?: boolean;
|
||||
fitContainer?: boolean;
|
||||
showCurrentCityMarker?: boolean;
|
||||
readonly?: boolean;
|
||||
}>(),
|
||||
{
|
||||
// Vue casts an absent Boolean prop to false unless undefined is an explicit default.
|
||||
@@ -404,6 +405,7 @@ const setHoveredCity = (cityId: number | null) => {
|
||||
};
|
||||
|
||||
const selectCity = (cityId: number) => {
|
||||
if (props.readonly) return;
|
||||
emit('select-city', cityId);
|
||||
if (props.selectedCityId === undefined) {
|
||||
mapStore.setSelectedCity(cityId);
|
||||
@@ -443,6 +445,7 @@ const selectCity = (cityId: number) => {
|
||||
:map-scale="mapScale"
|
||||
:show-name="showCityName"
|
||||
:select-only="props.selectedCityId !== undefined"
|
||||
:readonly="props.readonly"
|
||||
v-bind="detailProps"
|
||||
@hover="setHoveredCity"
|
||||
@leave="setHoveredCity(null)"
|
||||
|
||||
Reference in New Issue
Block a user