refac: linter 관련 설정 변경 및 적용, map_theme 변수 제거
- eslint에 prettier 조합 - prettierrc에 width 120, tabWidth 2 - gameStor->map_theme 제거 - map_theme, mapTheme를 GameConst::$mapName으로 대체 - eslint에서 vue/vue3-essential 대신 vue3-recommended 적용 - vue/max-attributes-per-line 완화 - vue/v-on-event-hyphenation 해제 - vue/attribute-hyphenation 해제 - 일부 tsc import type warning 해결 - 일부 vue template type warning 해결 - 일부 vue SFC를 script setup으로 변경 - TipTap - TopBackBar - BottomBackBar - BoardArticle - ProcessCity
This commit is contained in:
+383
-382
@@ -1,150 +1,159 @@
|
||||
<template>
|
||||
<div v-if="bettingDetailInfo !== undefined && info !== undefined">
|
||||
<div class="bg2">
|
||||
{{ info.name }}
|
||||
<span v-if="info.finished">(종료)</span>
|
||||
<span
|
||||
v-else-if="(yearMonth ?? 0) <= info.closeYearMonth"
|
||||
>({{ parseYearMonth(info.closeYearMonth)[0] }}년 {{ parseYearMonth(info.closeYearMonth)[1] }}월까지)</span>
|
||||
<span v-else>(베팅 마감)</span>
|
||||
(총액: {{ bettingAmount.toLocaleString() }})
|
||||
</div>
|
||||
|
||||
<div class="row bettingCandidates gx-1 gy-1">
|
||||
<div
|
||||
class="col-4 col-md-2"
|
||||
v-for="(candidate, idx) in info.candidates"
|
||||
:key="idx"
|
||||
@click="toggleCandidate(idx)"
|
||||
>
|
||||
<div
|
||||
:class="[
|
||||
'bettingCandidate',
|
||||
pickedBetType.has(idx) ? 'picked' : undefined,
|
||||
(info.finished && winner.has(idx)) ? 'picked' : undefined,
|
||||
]"
|
||||
>
|
||||
<div class="title bg1">{{ candidate.title }}</div>
|
||||
<div class="info" v-if="candidate.isHtml" v-html="candidate.info"></div>
|
||||
<div
|
||||
class="pickRate"
|
||||
>선택율: {{ ((partialBet.get(idx) ?? 0) / pureBettingAmount * 100).toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!info.finished && (yearMonth ?? 0) <= info.closeYearMonth" class="row gx-0">
|
||||
<div
|
||||
class="col-6 col-md-3 align-self-center"
|
||||
>잔여 {{ info.reqInheritancePoint ? '포인트' : '금' }} : {{ bettingDetailInfo.remainPoint.toLocaleString() }}</div>
|
||||
<div
|
||||
class="col-6 col-md-3 align-self-center"
|
||||
>사용 포인트: {{ sum(Array.from(myBettings.values())).toLocaleString() }}</div>
|
||||
<div class="col-6 col-md-3 align-self-center">대상: {{ getTypeStr(pickedBetTypeKey) }}</div>
|
||||
<div class="col-4 col-md-2 d-grid">
|
||||
<b-form-input
|
||||
class="d-grid"
|
||||
type="number"
|
||||
v-model="betPoint"
|
||||
:min="10"
|
||||
:max="1000"
|
||||
:step="10"
|
||||
></b-form-input>
|
||||
</div>
|
||||
<div class="col-2 col-md-1 d-grid">
|
||||
<b-button class="d-grid" @click="submitBet">베팅</b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="bg2">배당 순위</div>
|
||||
<div
|
||||
class="row"
|
||||
:style="{
|
||||
borderBottom: 'gray solid 1px'
|
||||
}"
|
||||
>
|
||||
<div class="col-5 text-center">대상</div>
|
||||
<div class="col-2 text-center">베팅액</div>
|
||||
<div class="col-3 text-center">내 베팅</div>
|
||||
<div class="col-2 text-center">{{ info.finished ? '배율' : '기대 배율' }}</div>
|
||||
</div>
|
||||
<template v-if="info.finished">
|
||||
<div class="row" v-for="[betType, amount] of detailBet" :key="betType">
|
||||
<template
|
||||
v-for="[matchPoint, color] of [calcMatchPointWithColor(betType)]"
|
||||
:key="matchPoint"
|
||||
>
|
||||
<div
|
||||
class="col-5"
|
||||
:style="{
|
||||
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
|
||||
color: color
|
||||
}"
|
||||
>{{ getTypeStr(betType) }}</div>
|
||||
<div class="col-2 text-end">{{ amount.toLocaleString() }}</div>
|
||||
<div class="col-3 text-center" v-if="myBettings.has(betType)">
|
||||
<template
|
||||
v-for="subPoint of [myBettings.get(betType) ?? 0]"
|
||||
>({{ subPoint.toLocaleString() }} -> {{ calculatedReward[matchPoint] == 0 ? 0 : (subPoint * calculatedReward[matchPoint] / (calculatedSubAmount.get(matchPoint) ?? 1)).toFixed(1).toLocaleString() }})</template>
|
||||
</div>
|
||||
<div class="col-3 text-center" v-else></div>
|
||||
<div
|
||||
class="col-2 text-end"
|
||||
>{{ (calculatedReward[matchPoint] == 0 ? 0 : (calculatedReward[matchPoint] / (calculatedSubAmount.get(matchPoint) ?? 1))).toFixed(1).toLocaleString() }}배</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="row" v-for="[betType, amount] of detailBet" :key="betType">
|
||||
<div
|
||||
class="col-5"
|
||||
:style="{
|
||||
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
|
||||
}"
|
||||
>{{ getTypeStr(betType) }}</div>
|
||||
<div class="col-2 text-end">{{ amount.toLocaleString() }}</div>
|
||||
<div class="col-3 text-center" v-if="myBettings.has(betType)">
|
||||
<template
|
||||
v-for="subPoint of [myBettings.get(betType) ?? 0]"
|
||||
>({{ subPoint.toLocaleString() }} -> {{ (subPoint * maxBettingReward / amount).toFixed(1).toLocaleString() }})</template>
|
||||
</div>
|
||||
<div class="col-3 text-center" v-else></div>
|
||||
<div
|
||||
class="col-2 text-end"
|
||||
>{{ (maxBettingReward / amount).toFixed(1).toLocaleString() }}배</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="bettingDetailInfo !== undefined && info !== undefined">
|
||||
<div class="bg2">
|
||||
{{ info.name }}
|
||||
<span v-if="info.finished">(종료)</span>
|
||||
<span v-else-if="(yearMonth ?? 0) <= info.closeYearMonth"
|
||||
>({{ parseYearMonth(info.closeYearMonth)[0] }}년 {{ parseYearMonth(info.closeYearMonth)[1] }}월까지)</span
|
||||
>
|
||||
<span v-else>(베팅 마감)</span>
|
||||
(총액: {{ bettingAmount.toLocaleString() }})
|
||||
</div>
|
||||
|
||||
<div class="row bettingCandidates gx-1 gy-1">
|
||||
<div v-for="(candidate, idx) in info.candidates" :key="idx" class="col-4 col-md-2" @click="toggleCandidate(idx)">
|
||||
<div
|
||||
:class="[
|
||||
'bettingCandidate',
|
||||
pickedBetType.has(idx) ? 'picked' : undefined,
|
||||
info.finished && winner.has(idx) ? 'picked' : undefined,
|
||||
]"
|
||||
>
|
||||
<div class="title bg1">
|
||||
{{ candidate.title }}
|
||||
</div>
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<div v-if="candidate.isHtml" class="info" v-html="candidate.info" />
|
||||
<div class="pickRate">선택율: {{ (((partialBet.get(idx) ?? 0) / pureBettingAmount) * 100).toFixed(1) }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!info.finished && (yearMonth ?? 0) <= info.closeYearMonth" class="row gx-0">
|
||||
<div class="col-6 col-md-3 align-self-center">
|
||||
잔여 {{ info.reqInheritancePoint ? "포인트" : "금" }} : {{ bettingDetailInfo.remainPoint.toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-6 col-md-3 align-self-center">
|
||||
사용 포인트: {{ sum(Array.from(myBettings.values())).toLocaleString() }}
|
||||
</div>
|
||||
<div class="col-6 col-md-3 align-self-center">대상: {{ getTypeStr(pickedBetTypeKey) }}</div>
|
||||
<div class="col-4 col-md-2 d-grid">
|
||||
<!-- eslint-disable-next-line vue/max-attributes-per-line -->
|
||||
<b-form-input v-model="betPoint" class="d-grid" type="number" :min="10" :max="1000" :step="10" />
|
||||
</div>
|
||||
<div class="col-2 col-md-1 d-grid">
|
||||
<b-button class="d-grid" @click="submitBet"> 베팅 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="bg2">배당 순위</div>
|
||||
<div
|
||||
class="row"
|
||||
:style="{
|
||||
borderBottom: 'gray solid 1px',
|
||||
}"
|
||||
>
|
||||
<div class="col-5 text-center">대상</div>
|
||||
<div class="col-2 text-center">베팅액</div>
|
||||
<div class="col-3 text-center">내 베팅</div>
|
||||
<div class="col-2 text-center">
|
||||
{{ info.finished ? "배율" : "기대 배율" }}
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="info.finished">
|
||||
<div v-for="[betType, amount] of detailBet" :key="betType" class="row">
|
||||
<template v-for="[matchPoint, color] of [calcMatchPointWithColor(betType)]" :key="matchPoint">
|
||||
<div
|
||||
class="col-5"
|
||||
:style="{
|
||||
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
|
||||
color: color,
|
||||
}"
|
||||
>
|
||||
{{ getTypeStr(betType) }}
|
||||
</div>
|
||||
<div class="col-2 text-end">
|
||||
{{ amount.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="myBettings.has(betType)" class="col-3 text-center">
|
||||
<template v-for="subPoint of [myBettings.get(betType) ?? 0]">
|
||||
({{ subPoint.toLocaleString() }} ->
|
||||
{{
|
||||
calculatedReward[matchPoint] == 0
|
||||
? 0
|
||||
: ((subPoint * calculatedReward[matchPoint]) / (calculatedSubAmount.get(matchPoint) ?? 1))
|
||||
.toFixed(1)
|
||||
.toLocaleString()
|
||||
}})
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="col-3 text-center" />
|
||||
<div class="col-2 text-end">
|
||||
{{
|
||||
(calculatedReward[matchPoint] == 0
|
||||
? 0
|
||||
: calculatedReward[matchPoint] / (calculatedSubAmount.get(matchPoint) ?? 1)
|
||||
)
|
||||
.toFixed(1)
|
||||
.toLocaleString()
|
||||
}}배
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div v-for="[betType, amount] of detailBet" :key="betType" class="row">
|
||||
<div
|
||||
class="col-5"
|
||||
:style="{
|
||||
fontWeight: myBettings.has(betType) ? 'bold' : undefined,
|
||||
}"
|
||||
>
|
||||
{{ getTypeStr(betType) }}
|
||||
</div>
|
||||
<div class="col-2 text-end">
|
||||
{{ amount.toLocaleString() }}
|
||||
</div>
|
||||
<div v-if="myBettings.has(betType)" class="col-3 text-center">
|
||||
<template v-for="subPoint of [myBettings.get(betType) ?? 0]">
|
||||
({{ subPoint.toLocaleString() }} ->
|
||||
{{ ((subPoint * maxBettingReward) / amount).toFixed(1).toLocaleString() }})
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="col-3 text-center" />
|
||||
<div class="col-2 text-end">{{ (maxBettingReward / amount).toFixed(1).toLocaleString() }}배</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { BettingInfo, ToastType } from '@/defs';
|
||||
import type { BettingInfo, ToastType } from "@/defs";
|
||||
import { SammoAPI, type ValidResponse } from "@/SammoAPI";
|
||||
import { joinYearMonth } from '@/util/joinYearMonth';
|
||||
import { parseYearMonth } from '@/util/parseYearMonth';
|
||||
import { isString, range, sum } from 'lodash';
|
||||
import { joinYearMonth } from "@/util/joinYearMonth";
|
||||
import { parseYearMonth } from "@/util/parseYearMonth";
|
||||
import { isString, range, sum } from "lodash";
|
||||
import { ref, type PropType, watch } from "vue";
|
||||
|
||||
type BettingDetailResponse = ValidResponse & {
|
||||
bettingInfo: BettingInfo;
|
||||
bettingDetail: [string, number][];
|
||||
myBetting: [string, number][];
|
||||
remainPoint: number;
|
||||
year: number;
|
||||
month: number;
|
||||
}
|
||||
|
||||
bettingInfo: BettingInfo;
|
||||
bettingDetail: [string, number][];
|
||||
myBetting: [string, number][];
|
||||
remainPoint: number;
|
||||
year: number;
|
||||
month: number;
|
||||
};
|
||||
|
||||
const props = defineProps({
|
||||
bettingID: {
|
||||
type: Number as PropType<number>,
|
||||
required: true,
|
||||
}
|
||||
bettingID: {
|
||||
type: Number as PropType<number>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'reqToast', content: ToastType): void,
|
||||
(event: "reqToast", content: ToastType): void;
|
||||
}>();
|
||||
|
||||
const year = ref<number>(0);
|
||||
@@ -163,24 +172,26 @@ const detailBet = ref<[string, number][]>([]);
|
||||
|
||||
const typeMap = ref(new Map<string, string>());
|
||||
function getTypeStr(type: string): string {
|
||||
const typeResult = typeMap.value.get(type);
|
||||
if (typeResult !== undefined) {
|
||||
return typeResult;
|
||||
}
|
||||
const bettingSubTypes = JSON.parse(type) as number[];
|
||||
if (bettingSubTypes[0] < -1) {
|
||||
return 'Invalid';
|
||||
}
|
||||
const typeResult = typeMap.value.get(type);
|
||||
if (typeResult !== undefined) {
|
||||
return typeResult;
|
||||
}
|
||||
const bettingSubTypes = JSON.parse(type) as number[];
|
||||
if (bettingSubTypes[0] < -1) {
|
||||
return "Invalid";
|
||||
}
|
||||
|
||||
const textBettingType = bettingSubTypes.map((idx) => {
|
||||
return bettingDetailInfo.value?.bettingInfo.candidates[idx].title;
|
||||
}).join(', ');
|
||||
typeMap.value.set(type, textBettingType);
|
||||
return textBettingType;
|
||||
const textBettingType = bettingSubTypes
|
||||
.map((idx) => {
|
||||
return bettingDetailInfo.value?.bettingInfo.candidates[idx].title;
|
||||
})
|
||||
.join(", ");
|
||||
typeMap.value.set(type, textBettingType);
|
||||
return textBettingType;
|
||||
}
|
||||
|
||||
const pickedBetType = ref(new Set<number>());
|
||||
const pickedBetTypeKey = ref('[]');
|
||||
const pickedBetTypeKey = ref("[]");
|
||||
|
||||
const betPoint = ref(0);
|
||||
const myBettings = ref(new Map<string, number>());
|
||||
@@ -189,274 +200,264 @@ const winner = ref(new Set<number>());
|
||||
const calculatedReward = ref<number[]>([]);
|
||||
const calculatedSubAmount = ref(new Map<number, number>());
|
||||
|
||||
function calcMatchPointWithColor(type: string): [number, "green" | "yellow" | "red" | undefined] {
|
||||
if (!info.value?.finished) {
|
||||
return [0, undefined];
|
||||
}
|
||||
const bettingSubTypes = JSON.parse(type) as number[];
|
||||
if (bettingSubTypes[0] < -1) {
|
||||
return [0, undefined];
|
||||
}
|
||||
|
||||
function calcMatchPointWithColor(type: string): [number, 'green' | 'yellow' | 'red' | undefined] {
|
||||
if (!info.value?.finished) {
|
||||
return [0, undefined];
|
||||
}
|
||||
const bettingSubTypes = JSON.parse(type) as number[];
|
||||
if (bettingSubTypes[0] < -1) {
|
||||
return [0, undefined];
|
||||
let matchPoint = 0;
|
||||
for (const subType of bettingSubTypes) {
|
||||
if (winner.value.has(subType)) {
|
||||
matchPoint += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let matchPoint = 0;
|
||||
for (const subType of bettingSubTypes) {
|
||||
if (winner.value.has(subType)) {
|
||||
matchPoint += 1;
|
||||
}
|
||||
if (info.value.isExclusive) {
|
||||
if (matchPoint == info.value.selectCnt) {
|
||||
return [matchPoint, "green"];
|
||||
} else {
|
||||
return [matchPoint, "red"];
|
||||
}
|
||||
}
|
||||
|
||||
if (info.value.isExclusive) {
|
||||
if (matchPoint == info.value.selectCnt) {
|
||||
return [matchPoint, 'green'];
|
||||
}
|
||||
else {
|
||||
return [matchPoint, 'red'];
|
||||
}
|
||||
}
|
||||
|
||||
let color: 'green' | 'red' | 'yellow' = 'green';
|
||||
if (matchPoint == 0) {
|
||||
color = 'red';
|
||||
}
|
||||
else if (matchPoint < info.value.selectCnt) {
|
||||
color = 'yellow';
|
||||
}
|
||||
return [matchPoint, color];
|
||||
let color: "green" | "red" | "yellow" = "green";
|
||||
if (matchPoint == 0) {
|
||||
color = "red";
|
||||
} else if (matchPoint < info.value.selectCnt) {
|
||||
color = "yellow";
|
||||
}
|
||||
return [matchPoint, color];
|
||||
}
|
||||
|
||||
|
||||
function calcReward() {
|
||||
if (info.value === undefined || bettingDetailInfo.value === undefined) {
|
||||
throw 'no info';
|
||||
}
|
||||
const selectCnt = info.value.selectCnt;
|
||||
const rewardAmount = new Array<number>(selectCnt).fill(0);
|
||||
|
||||
const subAmount = new Map<number, number>();
|
||||
for (const [bettingTypeStr, amount] of bettingDetailInfo.value.bettingDetail) {
|
||||
if (amount == 0) {
|
||||
continue;
|
||||
}
|
||||
const [matchPoint,] = calcMatchPointWithColor(bettingTypeStr);
|
||||
subAmount.set(matchPoint, (subAmount.get(matchPoint) ?? 0) + amount);
|
||||
}
|
||||
calculatedSubAmount.value = subAmount;
|
||||
|
||||
if (selectCnt == 1){
|
||||
rewardAmount[selectCnt - 1] = bettingAmount.value;
|
||||
calculatedReward.value = rewardAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.value.isExclusive) {
|
||||
rewardAmount[selectCnt - 1] = bettingAmount.value;
|
||||
calculatedReward.value = rewardAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
let remainRewardAmount = bettingAmount.value;
|
||||
|
||||
for (const matchPoint of range(selectCnt, 0, -1)) {
|
||||
if (!subAmount.has(matchPoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const givenRewardAmount = remainRewardAmount / 2;
|
||||
rewardAmount[matchPoint] = givenRewardAmount;
|
||||
remainRewardAmount -= givenRewardAmount; // /2가 아니라 다른 값이 될 경우를 대비..
|
||||
}
|
||||
|
||||
for (const matchPoint of range(1, selectCnt + 1)) {
|
||||
if (!subAmount.has(matchPoint)) {
|
||||
continue;
|
||||
}
|
||||
rewardAmount[matchPoint] += remainRewardAmount;
|
||||
break;
|
||||
if (info.value === undefined || bettingDetailInfo.value === undefined) {
|
||||
throw "no info";
|
||||
}
|
||||
const selectCnt = info.value.selectCnt;
|
||||
const rewardAmount = new Array<number>(selectCnt).fill(0);
|
||||
|
||||
const subAmount = new Map<number, number>();
|
||||
for (const [bettingTypeStr, amount] of bettingDetailInfo.value.bettingDetail) {
|
||||
if (amount == 0) {
|
||||
continue;
|
||||
}
|
||||
const [matchPoint] = calcMatchPointWithColor(bettingTypeStr);
|
||||
subAmount.set(matchPoint, (subAmount.get(matchPoint) ?? 0) + amount);
|
||||
}
|
||||
calculatedSubAmount.value = subAmount;
|
||||
|
||||
if (selectCnt == 1) {
|
||||
rewardAmount[selectCnt - 1] = bettingAmount.value;
|
||||
calculatedReward.value = rewardAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
if (info.value.isExclusive) {
|
||||
rewardAmount[selectCnt - 1] = bettingAmount.value;
|
||||
calculatedReward.value = rewardAmount;
|
||||
return;
|
||||
}
|
||||
|
||||
let remainRewardAmount = bettingAmount.value;
|
||||
|
||||
for (const matchPoint of range(selectCnt, 0, -1)) {
|
||||
if (!subAmount.has(matchPoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const givenRewardAmount = remainRewardAmount / 2;
|
||||
rewardAmount[matchPoint] = givenRewardAmount;
|
||||
remainRewardAmount -= givenRewardAmount; // /2가 아니라 다른 값이 될 경우를 대비..
|
||||
}
|
||||
|
||||
for (const matchPoint of range(1, selectCnt + 1)) {
|
||||
if (!subAmount.has(matchPoint)) {
|
||||
continue;
|
||||
}
|
||||
rewardAmount[matchPoint] += remainRewardAmount;
|
||||
break;
|
||||
}
|
||||
|
||||
calculatedReward.value = rewardAmount;
|
||||
}
|
||||
|
||||
async function loadBetting(bettingID: number) {
|
||||
try {
|
||||
const result = await SammoAPI.Betting.GetBettingDetail<BettingDetailResponse>({
|
||||
betting_id: bettingID
|
||||
});
|
||||
year.value = result.year;
|
||||
month.value = result.month;
|
||||
yearMonth.value = joinYearMonth(result.year, result.month);
|
||||
bettingDetailInfo.value = result;
|
||||
info.value = result.bettingInfo;
|
||||
try {
|
||||
const result = await SammoAPI.Betting.GetBettingDetail<BettingDetailResponse>({
|
||||
betting_id: bettingID,
|
||||
});
|
||||
year.value = result.year;
|
||||
month.value = result.month;
|
||||
yearMonth.value = joinYearMonth(result.year, result.month);
|
||||
bettingDetailInfo.value = result;
|
||||
info.value = result.bettingInfo;
|
||||
|
||||
partialBet.value.clear();
|
||||
partialBet.value.clear();
|
||||
|
||||
const betSort = new Map<string, number>();
|
||||
const betSort = new Map<string, number>();
|
||||
|
||||
|
||||
let _bettingAmount = 0;
|
||||
let adminBettingAmount = 0;
|
||||
for (const [bettingType, amount] of result.bettingDetail) {
|
||||
console.log(amount, typeof (amount));
|
||||
let userBet = true;
|
||||
const bettingSubTypes = JSON.parse(bettingType) as number[];
|
||||
for (const bettingSubType of bettingSubTypes) {
|
||||
if (bettingSubType < 0) {
|
||||
userBet = false;
|
||||
continue;
|
||||
}
|
||||
const oldValue = partialBet.value.get(bettingSubType) ?? 0;
|
||||
partialBet.value.set(bettingSubType, oldValue + amount);
|
||||
}
|
||||
|
||||
if (userBet) {
|
||||
const oldValue = betSort.get(bettingType) ?? 0;
|
||||
betSort.set(bettingType, oldValue + amount);
|
||||
}
|
||||
|
||||
_bettingAmount += amount;
|
||||
if (!userBet) {
|
||||
adminBettingAmount += amount;
|
||||
}
|
||||
}
|
||||
console.log(_bettingAmount);
|
||||
bettingAmount.value = _bettingAmount;
|
||||
pureBettingAmount.value = _bettingAmount - adminBettingAmount;
|
||||
if (info.value.isExclusive || info.value.selectCnt == 1) {
|
||||
maxBettingReward.value = _bettingAmount;
|
||||
} else {
|
||||
maxBettingReward.value = _bettingAmount / 2;
|
||||
let _bettingAmount = 0;
|
||||
let adminBettingAmount = 0;
|
||||
for (const [bettingType, amount] of result.bettingDetail) {
|
||||
console.log(amount, typeof amount);
|
||||
let userBet = true;
|
||||
const bettingSubTypes = JSON.parse(bettingType) as number[];
|
||||
for (const bettingSubType of bettingSubTypes) {
|
||||
if (bettingSubType < 0) {
|
||||
userBet = false;
|
||||
continue;
|
||||
}
|
||||
const oldValue = partialBet.value.get(bettingSubType) ?? 0;
|
||||
partialBet.value.set(bettingSubType, oldValue + amount);
|
||||
}
|
||||
|
||||
detailBet.value = Array.from(betSort.entries());
|
||||
detailBet.value.sort(([, lhsVal], [, rhsVal]) => {
|
||||
return rhsVal - lhsVal;
|
||||
})
|
||||
if (userBet) {
|
||||
const oldValue = betSort.get(bettingType) ?? 0;
|
||||
betSort.set(bettingType, oldValue + amount);
|
||||
}
|
||||
|
||||
pickedBetType.value.clear();
|
||||
pickedBetTypeKey.value = '[]';
|
||||
myBettings.value.clear();
|
||||
|
||||
|
||||
if (result.bettingInfo.winner) {
|
||||
winner.value = new Set(result.bettingInfo.winner);
|
||||
}
|
||||
else {
|
||||
winner.value.clear();
|
||||
}
|
||||
|
||||
for (const [betType, amount] of result.myBetting) {
|
||||
myBettings.value.set(betType, amount);
|
||||
}
|
||||
calcReward();
|
||||
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
emit('reqToast', {
|
||||
content: {
|
||||
title: "에러",
|
||||
body: e
|
||||
},
|
||||
options: {
|
||||
variant: 'danger',
|
||||
}
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
_bettingAmount += amount;
|
||||
if (!userBet) {
|
||||
adminBettingAmount += amount;
|
||||
}
|
||||
}
|
||||
console.log(_bettingAmount);
|
||||
bettingAmount.value = _bettingAmount;
|
||||
pureBettingAmount.value = _bettingAmount - adminBettingAmount;
|
||||
if (info.value.isExclusive || info.value.selectCnt == 1) {
|
||||
maxBettingReward.value = _bettingAmount;
|
||||
} else {
|
||||
maxBettingReward.value = _bettingAmount / 2;
|
||||
}
|
||||
|
||||
detailBet.value = Array.from(betSort.entries());
|
||||
detailBet.value.sort(([, lhsVal], [, rhsVal]) => {
|
||||
return rhsVal - lhsVal;
|
||||
});
|
||||
|
||||
pickedBetType.value.clear();
|
||||
pickedBetTypeKey.value = "[]";
|
||||
myBettings.value.clear();
|
||||
|
||||
if (result.bettingInfo.winner) {
|
||||
winner.value = new Set(result.bettingInfo.winner);
|
||||
} else {
|
||||
winner.value.clear();
|
||||
}
|
||||
|
||||
for (const [betType, amount] of result.myBetting) {
|
||||
myBettings.value.set(betType, amount);
|
||||
}
|
||||
calcReward();
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "에러",
|
||||
body: e,
|
||||
},
|
||||
options: {
|
||||
variant: "danger",
|
||||
},
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
void loadBetting(props.bettingID);
|
||||
watch(() => props.bettingID, (newBettingID) => {
|
||||
watch(
|
||||
() => props.bettingID,
|
||||
(newBettingID) => {
|
||||
void loadBetting(newBettingID);
|
||||
});
|
||||
|
||||
}
|
||||
);
|
||||
|
||||
function toggleCandidate(idx: number) {
|
||||
if (info.value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (bettingDetailInfo.value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (info.value.closeYearMonth < yearMonth.value) {
|
||||
return;
|
||||
}
|
||||
if (info.value.finished) {
|
||||
return;
|
||||
}
|
||||
const selectCnt = bettingDetailInfo.value.bettingInfo.selectCnt;
|
||||
if (info.value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (bettingDetailInfo.value === undefined) {
|
||||
return;
|
||||
}
|
||||
if (info.value.closeYearMonth < yearMonth.value) {
|
||||
return;
|
||||
}
|
||||
if (info.value.finished) {
|
||||
return;
|
||||
}
|
||||
const selectCnt = bettingDetailInfo.value.bettingInfo.selectCnt;
|
||||
|
||||
if (selectCnt == 1) {
|
||||
pickedBetType.value.clear();
|
||||
pickedBetType.value.add(idx);
|
||||
pickedBetTypeKey.value = JSON.stringify([idx]);
|
||||
return;
|
||||
}
|
||||
if (selectCnt == 1) {
|
||||
pickedBetType.value.clear();
|
||||
pickedBetType.value.add(idx);
|
||||
pickedBetTypeKey.value = JSON.stringify([idx]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (pickedBetType.value.has(idx)) {
|
||||
pickedBetType.value.delete(idx);
|
||||
}
|
||||
else if (pickedBetType.value.size < selectCnt) {
|
||||
pickedBetType.value.add(idx);
|
||||
}
|
||||
else {
|
||||
emit('reqToast', {
|
||||
content: {
|
||||
title: '오류',
|
||||
body: `이미 ${selectCnt}개를 선택했습니다.`,
|
||||
},
|
||||
options: {
|
||||
variant: 'warning',
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (pickedBetType.value.has(idx)) {
|
||||
pickedBetType.value.delete(idx);
|
||||
} else if (pickedBetType.value.size < selectCnt) {
|
||||
pickedBetType.value.add(idx);
|
||||
} else {
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "오류",
|
||||
body: `이미 ${selectCnt}개를 선택했습니다.`,
|
||||
},
|
||||
options: {
|
||||
variant: "warning",
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const typeArr = Array.from(pickedBetType.value.values());
|
||||
pickedBetTypeKey.value = JSON.stringify(typeArr.sort((lhs, rhs) => lhs - rhs));
|
||||
const typeArr = Array.from(pickedBetType.value.values());
|
||||
pickedBetTypeKey.value = JSON.stringify(typeArr.sort((lhs, rhs) => lhs - rhs));
|
||||
}
|
||||
|
||||
async function submitBet(): Promise<void> {
|
||||
const bettingInfo = info.value;
|
||||
if (bettingInfo === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bettingInfo = info.value;
|
||||
if (bettingInfo === undefined) {
|
||||
return;
|
||||
const bettingID = bettingInfo.id;
|
||||
const bettingType = JSON.parse(pickedBetTypeKey.value);
|
||||
const amount = betPoint.value;
|
||||
try {
|
||||
await SammoAPI.Betting.Bet({
|
||||
bettingID,
|
||||
bettingType,
|
||||
amount,
|
||||
});
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "완료",
|
||||
body: "베팅했습니다",
|
||||
},
|
||||
options: {
|
||||
variant: "success",
|
||||
},
|
||||
});
|
||||
await loadBetting(bettingInfo.id);
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
emit("reqToast", {
|
||||
content: {
|
||||
title: "에러",
|
||||
body: e,
|
||||
},
|
||||
options: {
|
||||
variant: "danger",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const bettingID = bettingInfo.id;
|
||||
const bettingType = JSON.parse(pickedBetTypeKey.value);
|
||||
const amount = betPoint.value;
|
||||
try {
|
||||
await SammoAPI.Betting.Bet({
|
||||
bettingID,
|
||||
bettingType,
|
||||
amount,
|
||||
});
|
||||
emit('reqToast', {
|
||||
content: {
|
||||
title: '완료',
|
||||
body: '베팅했습니다',
|
||||
},
|
||||
options: {
|
||||
variant: 'success'
|
||||
}
|
||||
});
|
||||
await loadBetting(bettingInfo.id);
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
emit('reqToast', {
|
||||
content: {
|
||||
title: "에러",
|
||||
body: e,
|
||||
},
|
||||
options: {
|
||||
variant: "danger",
|
||||
}
|
||||
});
|
||||
}
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,27 +1,26 @@
|
||||
<template>
|
||||
<div class="articleFrame bg0">
|
||||
<div class="bg1 row gx-0">
|
||||
<div class="authorName center">{{ article.author }}</div>
|
||||
<div class="col articleTitle center">{{ article.title }}</div>
|
||||
<div class="col-2 col-md-1 date center">{{ article.date.slice(5, 16) }}</div>
|
||||
<div class="authorName center">
|
||||
{{ article.author }}
|
||||
</div>
|
||||
<div class="col articleTitle center">
|
||||
{{ article.title }}
|
||||
</div>
|
||||
<div class="col-2 col-md-1 date center">
|
||||
{{ article.date.slice(5, 16) }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="row gx-0 s-border-b">
|
||||
<div class="col-2 col-md-1 authorIcon center">
|
||||
<img
|
||||
class="generalIcon"
|
||||
width="64"
|
||||
height="64"
|
||||
:src="article.author_icon"
|
||||
/>
|
||||
<img class="generalIcon" width="64" height="64" :src="article.author_icon" />
|
||||
</div>
|
||||
<div class="col text">
|
||||
{{ article.text }}
|
||||
</div>
|
||||
<div class="col text">{{ article.text }}</div>
|
||||
</div>
|
||||
<div class="commentList">
|
||||
<board-comment
|
||||
v-for="comment in article.comment"
|
||||
:key="comment.no"
|
||||
:comment="comment"
|
||||
/>
|
||||
<board-comment v-for="comment in article.comment" :key="comment.no" :comment="comment" />
|
||||
</div>
|
||||
<div class="row gx-0">
|
||||
<div class="bg2 inputCommentHeader center d-grid">
|
||||
@@ -29,86 +28,78 @@
|
||||
</div>
|
||||
<div class="col d-grid">
|
||||
<input
|
||||
v-model.trim="newCommentText"
|
||||
class="commentText"
|
||||
type="text"
|
||||
maxlength="250"
|
||||
placeholder="새 댓글 내용"
|
||||
v-model.trim="newCommentText"
|
||||
@keyup.enter="submitComment"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-2 col-md-1 d-grid">
|
||||
<b-button class="submitComment" @click="submitComment" size="sm"
|
||||
>등록</b-button
|
||||
>
|
||||
<b-button class="submitComment" size="sm" @click="submitComment"> 등록 </b-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import type { BoardArticleItem } from "@/PageBoard.vue";
|
||||
import BoardComment from "@/components/BoardComment.vue";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { ref, type PropType } from "vue";
|
||||
import axios from "axios";
|
||||
import { convertFormData } from "@util/convertFormData";
|
||||
import type { InvalidResponse } from "@/defs";
|
||||
export default defineComponent({
|
||||
name: "BoardArticle",
|
||||
components: {
|
||||
BoardComment,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
newCommentText: "",
|
||||
};
|
||||
},
|
||||
props: {
|
||||
article: {
|
||||
type: Object as PropType<BoardArticleItem>,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["submit-comment"],
|
||||
methods: {
|
||||
async submitComment() {
|
||||
const comment = this.newCommentText;
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
const articleNo = this.article.no;
|
||||
|
||||
let result: InvalidResponse;
|
||||
const newCommentText = ref("");
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_board_comment_add.php",
|
||||
method: "post",
|
||||
responseType: "json",
|
||||
data: convertFormData({
|
||||
articleNo: articleNo,
|
||||
text: comment,
|
||||
}),
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
this.newCommentText = "";
|
||||
|
||||
this.$emit("submit-comment");
|
||||
},
|
||||
const props = defineProps({
|
||||
article: {
|
||||
type: Object as PropType<BoardArticleItem>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "submit-comment"): void;
|
||||
}>();
|
||||
|
||||
async function submitComment() {
|
||||
const comment = newCommentText.value;
|
||||
if (!comment) {
|
||||
return;
|
||||
}
|
||||
const articleNo = props.article.no;
|
||||
|
||||
let result: InvalidResponse;
|
||||
|
||||
try {
|
||||
const response = await axios({
|
||||
url: "j_board_comment_add.php",
|
||||
method: "post",
|
||||
responseType: "json",
|
||||
data: convertFormData({
|
||||
articleNo: articleNo,
|
||||
text: comment,
|
||||
}),
|
||||
});
|
||||
result = response.data;
|
||||
if (!result.result) {
|
||||
throw result.reason;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
|
||||
newCommentText.value = "";
|
||||
|
||||
emit("submit-comment");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
td.text {
|
||||
white-space: pre;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
<template>
|
||||
<div class="row gx-0 comment s-border-b">
|
||||
<div class="authorName center d-grid"><div class="align-self-center">{{ comment.author }}</div></div>
|
||||
<div class="col text">{{ comment.text }}</div>
|
||||
<div class="col-2 col-md-1 date center d-grid"><div class="align-self-center">{{ comment.date.slice(5, 16) }}</div></div>
|
||||
<div class="authorName center d-grid">
|
||||
<div class="align-self-center">
|
||||
{{ comment.author }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col text">
|
||||
{{ comment.text }}
|
||||
</div>
|
||||
<div class="col-2 col-md-1 date center d-grid">
|
||||
<div class="align-self-center">
|
||||
{{ comment.date.slice(5, 16) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
|
||||
@@ -1,41 +1,30 @@
|
||||
<template>
|
||||
<div class="bg0" style="padding-top:20px;">
|
||||
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">
|
||||
돌아가기
|
||||
</button>
|
||||
<div></div>
|
||||
<div class="bg0" style="padding-top: 20px">
|
||||
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">돌아가기</button>
|
||||
<div />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from "vue";
|
||||
import "@scss/game_bg.scss";
|
||||
|
||||
export default defineComponent({
|
||||
methods: {
|
||||
back(){
|
||||
if(this.type === 'normal'){
|
||||
location.href = './';
|
||||
}
|
||||
else if(this.type == 'chief'){
|
||||
location.href = 'v_chiefCenter.php';
|
||||
}
|
||||
else{
|
||||
//TODO: window.close하려면 부모창이 있어야함!
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String as PropType<"normal"|"chief"|"close">,
|
||||
default: "normal",
|
||||
required: false,
|
||||
},
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<"normal" | "chief" | "close">,
|
||||
default: "normal",
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
|
||||
function back() {
|
||||
if (props.type === "normal") {
|
||||
location.href = "./";
|
||||
} else if (props.type == "chief") {
|
||||
location.href = "v_chiefCenter.php";
|
||||
} else {
|
||||
//TODO: window.close하려면 부모창이 있어야함!
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@@ -7,15 +7,11 @@
|
||||
:style="{
|
||||
color: getNpcColor(officer.npcType ?? 0),
|
||||
}"
|
||||
>{{ officer.name }}</div>
|
||||
>
|
||||
{{ officer.name }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
:class="[
|
||||
'row',
|
||||
'controlPad',
|
||||
props.targetIsMe ? 'targetIsMe' : 'targetIsNotMe',
|
||||
]"
|
||||
>
|
||||
<div :class="['row', 'controlPad', props.targetIsMe ? 'targetIsMe' : 'targetIsNotMe']">
|
||||
<div class="col-3 col-md-12 order-md-last">
|
||||
<div class="d-grid mb-1 py-1 only500px bg1 center">
|
||||
<div
|
||||
@@ -23,51 +19,50 @@
|
||||
color: getNpcColor(officer.npcType ?? 0),
|
||||
fontSize: '1.2em',
|
||||
}"
|
||||
>{{ officer.name }}</div>
|
||||
>
|
||||
{{ officer.name }}
|
||||
</div>
|
||||
<div>{{ officer.officerLevelText }}</div>
|
||||
</div>
|
||||
<div class="row gx-1 gy-1 py-1">
|
||||
<div class="col-md-4 mx-0 mb-0 mt-1 d-grid">
|
||||
<div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0"><SimpleClock :serverTime="parseTime(props.date)" /></div>
|
||||
<div class="alert alert-primary mb-0 center" style="padding: 0.5rem 0">
|
||||
<SimpleClock :serverTime="parseTime(props.date)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4 d-grid">
|
||||
<BButton
|
||||
variant="secondary"
|
||||
@click="isEditMode = !isEditMode"
|
||||
>{{ isEditMode ? '일반 모드' : '고급 모드' }}</BButton>
|
||||
<BButton variant="secondary" @click="isEditMode = !isEditMode">
|
||||
{{ isEditMode ? "일반 모드" : "고급 모드" }}
|
||||
</BButton>
|
||||
</div>
|
||||
|
||||
<BDropdown class="col-md-4" text="반복">
|
||||
<BDropdownItem
|
||||
v-for="turnIdx in maxPushTurn"
|
||||
:key="turnIdx"
|
||||
@click="repeatNationCommand(turnIdx)"
|
||||
>
|
||||
{{
|
||||
turnIdx
|
||||
}}턴
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="repeatNationCommand(turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
|
||||
<template v-if="isEditMode">
|
||||
<BDropdown class="col-md-4" left text="범위">
|
||||
<BDropdownItem @click="queryActionHelper.selectTurn()">해제</BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectAll()">모든턴</BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(0, 2)">홀수턴</BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(1, 2)">짝수턴</BDropdownItem>
|
||||
<BDropdownDivider></BDropdownDivider>
|
||||
<BDropdownItem @click="queryActionHelper.selectTurn()"> 해제 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectAll()"> 모든턴 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(0, 2)"> 홀수턴 </BDropdownItem>
|
||||
<BDropdownItem @click="queryActionHelper.selectStep(1, 2)"> 짝수턴 </BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
|
||||
<BDropdownText v-for="spanIdx in [3, 4, 5, 6, 7]" :key="spanIdx">
|
||||
{{ spanIdx }}턴 간격
|
||||
<br />
|
||||
<BButtonGroup>
|
||||
<BButton
|
||||
class="ignoreMe"
|
||||
v-for="beginIdx in spanIdx"
|
||||
:key="beginIdx"
|
||||
class="ignoreMe"
|
||||
@click="queryActionHelper.selectStep(beginIdx - 1, spanIdx)"
|
||||
>{{ beginIdx }}</BButton>
|
||||
>
|
||||
{{ beginIdx }}
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</BDropdownText>
|
||||
</BDropdown>
|
||||
@@ -79,7 +74,7 @@
|
||||
@click.self="useStoredAction(actions)"
|
||||
>
|
||||
{{ actionKey }}
|
||||
<BButton @click.prevent="deleteStoredActions(actionKey)" size="sm">삭제</BButton>
|
||||
<BButton size="sm" @click.prevent="deleteStoredActions(actionKey)"> 삭제 </BButton>
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
|
||||
@@ -90,34 +85,20 @@
|
||||
:key="idx"
|
||||
@click="void reserveCommandDirect([[Array.from(selectedTurnList.values()), action]])"
|
||||
>
|
||||
{{
|
||||
action.brief
|
||||
}}
|
||||
{{ action.brief }}
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<BDropdown class="col-md-6" split text="당기기" @click="pullNationCommandSingle">
|
||||
<BDropdownItem
|
||||
v-for="turnIdx in maxPushTurn"
|
||||
:key="turnIdx"
|
||||
@click="pushNationCommand(-turnIdx)"
|
||||
>
|
||||
{{
|
||||
turnIdx
|
||||
}}턴
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushNationCommand(-turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
<BDropdown class="col-md-6" split text="미루기" @click="pushNationCommandSingle">
|
||||
<BDropdownItem
|
||||
v-for="turnIdx in maxPushTurn"
|
||||
:key="turnIdx"
|
||||
@click="pushNationCommand(turnIdx)"
|
||||
>
|
||||
{{
|
||||
turnIdx
|
||||
}}턴
|
||||
<BDropdownItem v-for="turnIdx in maxPushTurn" :key="turnIdx" @click="pushNationCommand(turnIdx)">
|
||||
{{ turnIdx }}턴
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
@@ -134,28 +115,28 @@
|
||||
}"
|
||||
>
|
||||
<CommandSelectForm
|
||||
:commandList="commandList"
|
||||
ref="commandQuickReserveForm"
|
||||
@on-close="chooseQuickReserveCommand($event)"
|
||||
:hideClose="false"
|
||||
v-model:activatedCategory="activatedCategory"
|
||||
:commandList="commandList"
|
||||
:hideClose="false"
|
||||
class="bg-dark"
|
||||
style="position:absolute"
|
||||
style="position: absolute"
|
||||
@onClose="chooseQuickReserveCommand($event)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="commandPad chiefReservedCommand">
|
||||
<div :class="['commandTable', isEditMode ? 'editMode' : 'singleMode']">
|
||||
<DragSelect
|
||||
v-slot="{ selected }"
|
||||
:style="rowGridStyle"
|
||||
attribute="turnIdx"
|
||||
:disabled="!isEditMode"
|
||||
@dragStart="isDragSingle = true"
|
||||
@dragDone="
|
||||
isDragSingle = false;
|
||||
queryActionHelper.selectTurn(...$event);
|
||||
isDragSingle = false;
|
||||
queryActionHelper.selectTurn(...$event);
|
||||
"
|
||||
v-slot="{ selected }"
|
||||
>
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList"
|
||||
@@ -166,21 +147,22 @@ queryActionHelper.selectTurn(...$event);
|
||||
backgroundColor: 'black',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
color:
|
||||
isDragSingle && selected.has(`${turnIdx}`) ? 'cyan' : undefined,
|
||||
color: isDragSingle && selected.has(`${turnIdx}`) ? 'cyan' : undefined,
|
||||
}"
|
||||
>{{ turnObj.time }}</div>
|
||||
>
|
||||
{{ turnObj.time }}
|
||||
</div>
|
||||
</DragSelect>
|
||||
<DragSelect
|
||||
v-slot="{ selected }"
|
||||
:style="{ ...rowGridStyle, display: isEditMode ? 'grid' : 'none' }"
|
||||
attribute="turnIdx"
|
||||
:disabled="!isEditMode"
|
||||
@dragStart="isDragToggle = true"
|
||||
@dragDone="
|
||||
isDragToggle = false;
|
||||
toggleTurn(...$event);
|
||||
isDragToggle = false;
|
||||
toggleTurn(...$event);
|
||||
"
|
||||
v-slot="{ selected }"
|
||||
>
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList"
|
||||
@@ -194,90 +176,75 @@ toggleTurn(...$event);
|
||||
isDragToggle && selected.has(`${turnIdx}`)
|
||||
? 'light'
|
||||
: selectedTurnList.has(turnIdx)
|
||||
? 'info'
|
||||
: selectedTurnList.size == 0 && prevSelectedTurnList.has(turnIdx)
|
||||
? 'success'
|
||||
: 'primary'
|
||||
? 'info'
|
||||
: selectedTurnList.size == 0 && prevSelectedTurnList.has(turnIdx)
|
||||
? 'success'
|
||||
: 'primary'
|
||||
"
|
||||
>{{ turnIdx + 1 }}</BButton>
|
||||
>
|
||||
{{ turnIdx + 1 }}
|
||||
</BButton>
|
||||
</div>
|
||||
</DragSelect>
|
||||
<div :style="rowGridStyle">
|
||||
<div
|
||||
v-for="(turnObj, turnIdx) in reservedCommandList"
|
||||
:key="turnIdx"
|
||||
class="turn_pad center"
|
||||
>
|
||||
<span
|
||||
class="turn_text"
|
||||
:style="turnObj.style"
|
||||
v-b-tooltip.hover
|
||||
:title="turnObj.tooltip"
|
||||
v-html="turnObj.brief"
|
||||
></span>
|
||||
<div v-for="(turnObj, turnIdx) in reservedCommandList" :key="turnIdx" class="turn_pad center">
|
||||
<span v-b-tooltip.hover class="turn_text" :style="turnObj.style" :title="turnObj.tooltip">
|
||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||
<span v-html="turnObj.brief" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div :style="{ ...rowGridStyle, display: isEditMode ? 'none' : 'grid' }">
|
||||
<div v-for="turnIdx in range(props.maxTurn)" :key="turnIdx" class="action_pad d-grid">
|
||||
<BButton
|
||||
:variant="(turnIdx % 2 == 0) ? 'secondary' : 'dark'"
|
||||
:variant="turnIdx % 2 == 0 ? 'secondary' : 'dark'"
|
||||
size="sm"
|
||||
class="simple_action_btn bi bi-pencil"
|
||||
@click="toggleQuickReserveForm(turnIdx)"
|
||||
></BButton>
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="position:relative">
|
||||
<div style="position: relative">
|
||||
<CommandSelectForm
|
||||
:commandList="commandList"
|
||||
ref="commandSelectForm"
|
||||
@on-close="chooseCommand($event)"
|
||||
v-model:activatedCategory="activatedCategory"
|
||||
:commandList="commandList"
|
||||
class="bg-dark"
|
||||
:style="{ position: 'absolute', bottom: '0' }"
|
||||
@onClose="chooseCommand($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="row gx-0" v-if="isEditMode">
|
||||
<div v-if="isEditMode" class="row gx-0">
|
||||
<div class="col-5 col-md-6 d-grid">
|
||||
<BDropdown left variant="info" text="선택한 턴을">
|
||||
<BDropdownItem @click="clipboardCut">
|
||||
<i class="bi bi-scissors"></i> 잘라내기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="clipboardCopy">
|
||||
<i class="bi bi-files"></i> 복사하기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="clipboardCut"> <i class="bi bi-scissors" /> 잘라내기 </BDropdownItem>
|
||||
<BDropdownItem @click="clipboardCopy"> <i class="bi bi-files" /> 복사하기 </BDropdownItem>
|
||||
<BDropdownItem @click="clipboardPaste">
|
||||
<i class="bi bi-clipboard-fill"></i> 붙여넣기
|
||||
<i class="bi bi-clipboard-fill" /> 붙여넣기
|
||||
</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem @click="setStoredActions">
|
||||
<i class="bi bi-bookmark-plus-fill"></i> 보관하기
|
||||
<i class="bi bi-bookmark-plus-fill" /> 보관하기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="subRepeatCommand">
|
||||
<i class="bi bi-arrow-repeat"></i> 반복하기
|
||||
<i class="bi bi-arrow-repeat" /> 반복하기
|
||||
</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem @click="eraseSelectedTurnList">
|
||||
<i class="bi bi-eraser"></i> 비우기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="eraseSelectedTurnList"> <i class="bi bi-eraser" /> 비우기 </BDropdownItem>
|
||||
<BDropdownItem @click="eraseAndPullCommand">
|
||||
<i class="bi bi-arrow-bar-up"></i> 지우고 당기기
|
||||
<i class="bi bi-arrow-bar-up" /> 지우고 당기기
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="pushEmptyCommand">
|
||||
<i class="bi bi-arrow-bar-down"></i> 뒤로 밀기
|
||||
<i class="bi bi-arrow-bar-down" /> 뒤로 밀기
|
||||
</BDropdownItem>
|
||||
<!-- 최근에 실행한 10턴 -->
|
||||
</BDropdown>
|
||||
</div>
|
||||
|
||||
<div class="col-7 col-md-6 d-grid">
|
||||
<BButton
|
||||
variant="light"
|
||||
@click="toggleForm($event)"
|
||||
:style="{ color: 'black' }"
|
||||
>명령 선택 ▾</BButton>
|
||||
<BButton variant="light" :style="{ color: 'black' }" @click="toggleForm($event)"> 명령 선택 ▾ </BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,7 +294,6 @@ const props = defineProps({
|
||||
turnTime: VueTypes.string.isRequired,
|
||||
targetIsMe: VueTypes.bool.isRequired,
|
||||
|
||||
|
||||
selectedTurn: {
|
||||
type: Object as PropType<Set<number>>,
|
||||
required: false,
|
||||
@@ -338,15 +304,15 @@ const props = defineProps({
|
||||
required: true,
|
||||
},
|
||||
commandList: {
|
||||
type: Object as PropType<ChiefResponse['commandList']>,
|
||||
type: Object as PropType<ChiefResponse["commandList"]>,
|
||||
required: true,
|
||||
},
|
||||
|
||||
officer: {
|
||||
type: Object as PropType<ChiefResponse['chiefList'][0]>,
|
||||
type: Object as PropType<ChiefResponse["chiefList"][0]>,
|
||||
required: true,
|
||||
}
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
const basicModeRowHeight = 30;
|
||||
|
||||
@@ -391,8 +357,8 @@ const isDragToggle = ref(false);
|
||||
const autorun_limit = ref<number | null>(null);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'raiseReload'): void,
|
||||
(event: 'update:selectedTurn', value: Set<number>): void,
|
||||
(event: "raise-reload"): void;
|
||||
(event: "update:selectedTurn", value: Set<number>): void;
|
||||
}>();
|
||||
|
||||
function triggerUpdateCommandList(type?: string) {
|
||||
@@ -442,10 +408,9 @@ async function repeatNationCommand(amount: number) {
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
emit('raiseReload');
|
||||
emit("raise-reload");
|
||||
}
|
||||
|
||||
|
||||
function pushNationCommandSingle(e: Event) {
|
||||
//NOTE: split 구현에 버그가 있어서, 수동으로 구분해야함
|
||||
if (isDropdownChildren(e)) {
|
||||
@@ -470,10 +435,9 @@ async function pushNationCommand(amount: number) {
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
emit('raiseReload');
|
||||
emit("raise-reload");
|
||||
}
|
||||
|
||||
|
||||
const queryActionHelper = new QueryActionHelper(props.maxTurn);
|
||||
const reservedCommandList = queryActionHelper.reservedCommandList;
|
||||
const prevSelectedTurnList = queryActionHelper.prevSelectedTurnList;
|
||||
@@ -481,15 +445,15 @@ const selectedTurnList = queryActionHelper.selectedTurnList;
|
||||
|
||||
async function reserveCommandDirect(args: [number[], TurnObj][], reload = true): Promise<boolean> {
|
||||
const query: {
|
||||
turnList: number[],
|
||||
action: string,
|
||||
arg: Args
|
||||
turnList: number[];
|
||||
action: string;
|
||||
arg: Args;
|
||||
}[] = [];
|
||||
for (const [turnList, { action, arg }] of args) {
|
||||
query.push({
|
||||
turnList,
|
||||
action,
|
||||
arg
|
||||
arg,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -503,7 +467,7 @@ async function reserveCommandDirect(args: [number[], TurnObj][], reload = true):
|
||||
}
|
||||
|
||||
if (reload) {
|
||||
emit('raiseReload');
|
||||
emit("raise-reload");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -520,9 +484,7 @@ function updateCommandList() {
|
||||
let nextTurnTime = new Date(turnTime);
|
||||
|
||||
const autorunLimitYearMonth = autorun_limit.value ?? yearMonth - 1;
|
||||
const [autorunLimitYear, autorunLimitMonth] = parseYearMonth(
|
||||
autorunLimitYearMonth
|
||||
);
|
||||
const [autorunLimitYear, autorunLimitMonth] = parseYearMonth(autorunLimitYearMonth);
|
||||
|
||||
for (const obj of props.turn) {
|
||||
const [year, month] = parseYearMonth(yearMonth);
|
||||
@@ -537,9 +499,7 @@ function updateCommandList() {
|
||||
}
|
||||
style.color = "#aaffff";
|
||||
|
||||
tooltip.push(
|
||||
`자율 행동 기간: ${autorunLimitYear}년 ${autorunLimitMonth}월까지`
|
||||
);
|
||||
tooltip.push(`자율 행동 기간: ${autorunLimitYear}년 ${autorunLimitMonth}월까지`);
|
||||
}
|
||||
|
||||
if (mb_strwidth(brief) > 22) {
|
||||
@@ -550,10 +510,7 @@ function updateCommandList() {
|
||||
...obj,
|
||||
year,
|
||||
month,
|
||||
time: formatTime(
|
||||
nextTurnTime,
|
||||
props.turnTerm >= 5 ? "HH:mm" : "mm:ss"
|
||||
),
|
||||
time: formatTime(nextTurnTime, props.turnTerm >= 5 ? "HH:mm" : "mm:ss"),
|
||||
tooltip: tooltip.length == 0 ? undefined : tooltip.join("\n"),
|
||||
style,
|
||||
});
|
||||
@@ -590,7 +547,7 @@ async function reserveCommand() {
|
||||
storedActionsHelper.pushRecentActions({
|
||||
action: commandName,
|
||||
brief: result.brief,
|
||||
arg: {}
|
||||
arg: {},
|
||||
});
|
||||
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
@@ -599,7 +556,7 @@ async function reserveCommand() {
|
||||
alert(`실패했습니다: ${e}`);
|
||||
return;
|
||||
}
|
||||
emit("raiseReload");
|
||||
emit("raise-reload");
|
||||
}
|
||||
|
||||
function chooseCommand(val?: string) {
|
||||
@@ -610,20 +567,16 @@ function chooseCommand(val?: string) {
|
||||
void reserveCommand();
|
||||
}
|
||||
|
||||
const emptyTurnObj: TurnObj = { action: '휴식', brief: '휴식', arg: {} };
|
||||
const emptyTurnObj: TurnObj = { action: "휴식", brief: "휴식", arg: {} };
|
||||
|
||||
|
||||
const storedActionsHelper = inject('storedNationActionsHelper') as StoredActionsHelper;
|
||||
const storedActionsHelper = inject("storedNationActionsHelper") as StoredActionsHelper;
|
||||
|
||||
const recentActions = storedActionsHelper.recentActions;
|
||||
const storedActions = storedActionsHelper.storedActions;
|
||||
const activatedCategory = storedActionsHelper.activatedCategory;
|
||||
|
||||
async function eraseSelectedTurnList(releaseSelect = true): Promise<boolean> {
|
||||
const result = await reserveCommandDirect([[
|
||||
queryActionHelper.getSelectedTurnList(),
|
||||
emptyTurnObj
|
||||
]]);
|
||||
const result = await reserveCommandDirect([[queryActionHelper.getSelectedTurnList(), emptyTurnObj]]);
|
||||
if (releaseSelect) {
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
}
|
||||
@@ -669,7 +622,10 @@ async function subRepeatCommand(releaseSelect = true): Promise<boolean> {
|
||||
const queryLength = selectedMaxTurnIdx - selectedMinTurnIdx + 1;
|
||||
|
||||
const rawActions = queryActionHelper.extractQueryActions();
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, range(selectedMinTurnIdx, props.maxTurn, queryLength));
|
||||
const actions = queryActionHelper.amplifyQueryActions(
|
||||
rawActions,
|
||||
range(selectedMinTurnIdx, props.maxTurn, queryLength)
|
||||
);
|
||||
|
||||
const result = await reserveCommandDirect(actions);
|
||||
if (releaseSelect) {
|
||||
@@ -678,7 +634,6 @@ async function subRepeatCommand(releaseSelect = true): Promise<boolean> {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const selectedMinTurnIdx = reqTurnList[0];
|
||||
@@ -696,7 +651,6 @@ async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
|
||||
|
||||
const actions: [number[], TurnObj][] = [];
|
||||
|
||||
|
||||
const emptyTurnList: number[] = [];
|
||||
|
||||
for (const srcTurnIdx of range(selectedMinTurnIdx + queryLength, props.maxTurn)) {
|
||||
@@ -705,11 +659,14 @@ async function eraseAndPullCommand(releaseSelect = true): Promise<boolean> {
|
||||
emptyTurnList.push(srcTurnIdx - queryLength);
|
||||
continue;
|
||||
}
|
||||
actions.push([[srcTurnIdx - queryLength], {
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief
|
||||
}]);
|
||||
actions.push([
|
||||
[srcTurnIdx - queryLength],
|
||||
{
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
emptyTurnList.push(...range(props.maxTurn - queryLength, props.maxTurn));
|
||||
@@ -739,7 +696,6 @@ async function pushEmptyCommand(releaseSelect = true): Promise<boolean> {
|
||||
|
||||
const actions: [number[], TurnObj][] = [];
|
||||
|
||||
|
||||
const emptyTurnList: number[] = [];
|
||||
|
||||
for (const srcTurnIdx of range(selectedMinTurnIdx, props.maxTurn - queryLength)) {
|
||||
@@ -748,11 +704,14 @@ async function pushEmptyCommand(releaseSelect = true): Promise<boolean> {
|
||||
emptyTurnList.push(srcTurnIdx + queryLength);
|
||||
continue;
|
||||
}
|
||||
actions.push([[srcTurnIdx + queryLength], {
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief
|
||||
}]);
|
||||
actions.push([
|
||||
[srcTurnIdx + queryLength],
|
||||
{
|
||||
action: rawAction.action,
|
||||
arg: rawAction.arg,
|
||||
brief: rawAction.brief,
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
emptyTurnList.push(...range(selectedMinTurnIdx, selectedMinTurnIdx + queryLength));
|
||||
@@ -769,7 +728,7 @@ function setStoredActions() {
|
||||
const actions = queryActionHelper.extractQueryActions();
|
||||
const turnBrief = new Map<number, string>();
|
||||
for (const [subTurnList, action] of actions) {
|
||||
const actionName = action.action.split('_');
|
||||
const actionName = action.action.split("_");
|
||||
const actionShortName = actionName.length == 1 ? actionName[0] : actionName[1];
|
||||
for (const turnIdx of subTurnList) {
|
||||
turnBrief.set(turnIdx, actionShortName[0]);
|
||||
@@ -779,10 +738,10 @@ function setStoredActions() {
|
||||
const turnBriefStr = Array.from(turnBrief.entries())
|
||||
.sort(([turnA], [turnB]) => turnA - turnB)
|
||||
.map(([, action]) => action)
|
||||
.join('');
|
||||
.join("");
|
||||
|
||||
const nickName = trim(prompt('선택한 턴들의 별명을 지어주세요', turnBriefStr) ?? '');
|
||||
if (nickName == '') {
|
||||
const nickName = trim(prompt("선택한 턴들의 별명을 지어주세요", turnBriefStr) ?? "");
|
||||
if (nickName == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -791,12 +750,12 @@ function setStoredActions() {
|
||||
}
|
||||
|
||||
function deleteStoredActions(actionKey: string) {
|
||||
storedActionsHelper.deleteStoredActions(actionKey)
|
||||
storedActionsHelper.deleteStoredActions(actionKey);
|
||||
}
|
||||
|
||||
async function useStoredAction(rawActions: [number[], TurnObj][]) {
|
||||
const reqTurnList = queryActionHelper.getSelectedTurnList();
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, reqTurnList)
|
||||
const actions = queryActionHelper.amplifyQueryActions(rawActions, reqTurnList);
|
||||
const result = await reserveCommandDirect(actions);
|
||||
queryActionHelper.releaseSelectedTurnList();
|
||||
return result;
|
||||
@@ -818,39 +777,57 @@ defineExpose({
|
||||
clipboardPaste,
|
||||
getQueryActionHelper,
|
||||
getStoredActionHeler,
|
||||
})
|
||||
});
|
||||
|
||||
watch(() => props.date, () => {
|
||||
triggerUpdateCommandList("date");
|
||||
})
|
||||
watch(() => props.year, () => {
|
||||
triggerUpdateCommandList("year");
|
||||
})
|
||||
watch(() => props.month, () => {
|
||||
triggerUpdateCommandList("month");
|
||||
})
|
||||
watch(() => props.turnTime, () => {
|
||||
triggerUpdateCommandList("turnTime");
|
||||
})
|
||||
watch(() => props.commandList, () => {
|
||||
triggerUpdateCommandList("commandList");
|
||||
})
|
||||
watch(() => props.selectedTurn, (val: Set<number>) => {
|
||||
console.log(val);
|
||||
if (val === selectedTurnList.value) {
|
||||
console.log("pass!");
|
||||
return;
|
||||
watch(
|
||||
() => props.date,
|
||||
() => {
|
||||
triggerUpdateCommandList("date");
|
||||
}
|
||||
selectedTurnList.value.clear();
|
||||
for (const t of val.values()) {
|
||||
selectedTurnList.value.add(t);
|
||||
);
|
||||
watch(
|
||||
() => props.year,
|
||||
() => {
|
||||
triggerUpdateCommandList("year");
|
||||
}
|
||||
})
|
||||
);
|
||||
watch(
|
||||
() => props.month,
|
||||
() => {
|
||||
triggerUpdateCommandList("month");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.turnTime,
|
||||
() => {
|
||||
triggerUpdateCommandList("turnTime");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.commandList,
|
||||
() => {
|
||||
triggerUpdateCommandList("commandList");
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() => props.selectedTurn,
|
||||
(val: Set<number>) => {
|
||||
console.log(val);
|
||||
if (val === selectedTurnList.value) {
|
||||
console.log("pass!");
|
||||
return;
|
||||
}
|
||||
selectedTurnList.value.clear();
|
||||
for (const t of val.values()) {
|
||||
selectedTurnList.value.add(t);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(selectedTurnList, () => {
|
||||
console.log(selectedTurnList.value);
|
||||
emit("update:selectedTurn", selectedTurnList.value);
|
||||
})
|
||||
});
|
||||
|
||||
const commandQuickReserveForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
|
||||
const commandSelectForm = ref<InstanceType<typeof CommandSelectForm> | null>(null);
|
||||
@@ -876,12 +853,11 @@ function toggleQuickReserveForm(turnIdx: number) {
|
||||
}
|
||||
|
||||
const isEditMode = storedActionsHelper.isEditMode;
|
||||
watch(isEditMode, newEditMode => {
|
||||
watch(isEditMode, (newEditMode) => {
|
||||
if (newEditMode) {
|
||||
commandQuickReserveForm.value?.close();
|
||||
currentQuickReserveTarget.value = -1;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
commandSelectForm.value?.close();
|
||||
}
|
||||
});
|
||||
@@ -897,8 +873,7 @@ function toggleForm($event: Event): void {
|
||||
|
||||
onMounted(() => {
|
||||
updateCommandList();
|
||||
})
|
||||
|
||||
});
|
||||
</script>
|
||||
<style lang="scss">
|
||||
@import "@scss/common/break_500px.scss";
|
||||
@@ -990,4 +965,4 @@ onMounted(() => {
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -1,68 +1,58 @@
|
||||
<template>
|
||||
<div v-if="showForm" class="my-1">
|
||||
<div class="commandCategory row gx-0 gy-1">
|
||||
<div
|
||||
class="categoryItem col-4 d-grid"
|
||||
v-for="[categoryKey, { deco: categoryDeco }] of commandList"
|
||||
:key="categoryKey"
|
||||
>
|
||||
<BButton
|
||||
variant="success"
|
||||
@click="chosenCategory = categoryKey"
|
||||
:active="chosenCategory == categoryKey"
|
||||
>{{ categoryDeco.altName ?? categoryDeco.name }}</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="commandList my-1"
|
||||
:style="{
|
||||
display: 'grid',
|
||||
alignItems: 'self-start',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="row gx-1 gy-1"
|
||||
v-for="[category, { values }] of commandList"
|
||||
:key="category"
|
||||
:style="{ visibility: category == chosenCategory ? 'visible' : 'hidden', gridRow: '1', gridColumn: '1' }"
|
||||
>
|
||||
<div
|
||||
class="col-6 d-grid"
|
||||
v-for="commandItem of values"
|
||||
:key="commandItem.value"
|
||||
@click="close(commandItem.value)"
|
||||
>
|
||||
<div class="commandItem">
|
||||
<p
|
||||
:class="['center', 'my-0', commandItem.possible ? '' : 'commandImpossible']"
|
||||
>
|
||||
{{ commandItem.simpleName }}
|
||||
<span
|
||||
class="compensatePositive"
|
||||
v-if="commandItem.compensation > 0"
|
||||
>▲</span>
|
||||
<span
|
||||
class="compensateNegative"
|
||||
v-else-if="commandItem.compensation < 0"
|
||||
>▼</span>
|
||||
</p>
|
||||
<small class="center" :style="{ display: 'block' }">
|
||||
{{
|
||||
commandItem.title.startsWith(commandItem.simpleName)
|
||||
? commandItem.title.substring(commandItem.simpleName.length)
|
||||
: commandItem.title
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!hideClose" class="commandBottom row mt-1 mb-1">
|
||||
<div class="offset-8 col-4 d-grid">
|
||||
<BButton @click="close()">닫기</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="showForm" class="my-1">
|
||||
<div class="commandCategory row gx-0 gy-1">
|
||||
<div
|
||||
v-for="[categoryKey, { deco: categoryDeco }] of commandList"
|
||||
:key="categoryKey"
|
||||
class="categoryItem col-4 d-grid"
|
||||
>
|
||||
<BButton variant="success" :active="chosenCategory == categoryKey" @click="chosenCategory = categoryKey">
|
||||
{{ categoryDeco.altName ?? categoryDeco.name }}
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="commandList my-1"
|
||||
:style="{
|
||||
display: 'grid',
|
||||
alignItems: 'self-start',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="[category, { values }] of commandList"
|
||||
:key="category"
|
||||
class="row gx-1 gy-1"
|
||||
:style="{ visibility: category == chosenCategory ? 'visible' : 'hidden', gridRow: '1', gridColumn: '1' }"
|
||||
>
|
||||
<div
|
||||
v-for="commandItem of values"
|
||||
:key="commandItem.value"
|
||||
class="col-6 d-grid"
|
||||
@click="close(commandItem.value)"
|
||||
>
|
||||
<div class="commandItem">
|
||||
<p :class="['center', 'my-0', commandItem.possible ? '' : 'commandImpossible']">
|
||||
{{ commandItem.simpleName }}
|
||||
<span v-if="commandItem.compensation > 0" class="compensatePositive">▲</span>
|
||||
<span v-else-if="commandItem.compensation < 0" class="compensateNegative">▼</span>
|
||||
</p>
|
||||
<small class="center" :style="{ display: 'block' }">
|
||||
{{
|
||||
commandItem.title.startsWith(commandItem.simpleName)
|
||||
? commandItem.title.substring(commandItem.simpleName.length)
|
||||
: commandItem.title
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!hideClose" class="commandBottom row mt-1 mb-1">
|
||||
<div class="offset-8 col-4 d-grid">
|
||||
<BButton @click="close()"> 닫기 </BButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { CommandItem } from "@/defs";
|
||||
@@ -70,144 +60,152 @@ import { BButton } from "bootstrap-vue-3";
|
||||
import { ref, type PropType, watch, onMounted } from "vue";
|
||||
|
||||
interface CategoryDecoration {
|
||||
name: string,
|
||||
altName?: string,
|
||||
//icon?: string,
|
||||
//color?: string,
|
||||
//backgroundColor?: string,
|
||||
name: string;
|
||||
altName?: string;
|
||||
//icon?: string,
|
||||
//color?: string,
|
||||
//backgroundColor?: string,
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
categoryInfo: {
|
||||
type: Object as PropType<Record<string, Omit<CategoryDecoration, 'name'>>>,
|
||||
required: false,
|
||||
categoryInfo: {
|
||||
type: Object as PropType<Record<string, Omit<CategoryDecoration, "name">>>,
|
||||
default: () => {
|
||||
return {};
|
||||
},
|
||||
commandList: {
|
||||
type: Object as PropType<{
|
||||
category: string;
|
||||
values: CommandItem[];
|
||||
}[]>,
|
||||
required: true,
|
||||
},
|
||||
anchor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '.commandSelectFormAnchor',
|
||||
},
|
||||
hideClose: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
activatedCategory: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
}
|
||||
})
|
||||
required: false,
|
||||
},
|
||||
commandList: {
|
||||
type: Object as PropType<
|
||||
{
|
||||
category: string;
|
||||
values: CommandItem[];
|
||||
}[]
|
||||
>,
|
||||
required: true,
|
||||
},
|
||||
anchor: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: ".commandSelectFormAnchor",
|
||||
},
|
||||
hideClose: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
activatedCategory: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "",
|
||||
},
|
||||
});
|
||||
|
||||
const chosenCategory = ref<string>('-');
|
||||
const chosenCategory = ref<string>("-");
|
||||
const chosenSubList = ref<CommandItem[]>([]);
|
||||
|
||||
const categories = new Set(props.commandList.map(({ category }) => category));
|
||||
|
||||
watch(() => props.activatedCategory, (newValue) => {
|
||||
watch(
|
||||
() => props.activatedCategory,
|
||||
(newValue) => {
|
||||
chosenCategory.value = newValue;
|
||||
})
|
||||
}
|
||||
);
|
||||
|
||||
const showForm = ref(false);
|
||||
|
||||
function convCategoryDeco(category: string): CategoryDecoration {
|
||||
const itemInfo = props.categoryInfo?.[category];
|
||||
if (!itemInfo) {
|
||||
return {
|
||||
name: category,
|
||||
}
|
||||
}
|
||||
const itemInfo = props.categoryInfo?.[category];
|
||||
if (!itemInfo) {
|
||||
return {
|
||||
name: category,
|
||||
...itemInfo
|
||||
name: category,
|
||||
};
|
||||
}
|
||||
return {
|
||||
name: category,
|
||||
...itemInfo,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
const commandList = ref(new Map<string, {
|
||||
deco: CategoryDecoration,
|
||||
values: CommandItem[],
|
||||
}>());
|
||||
const commandList = ref(
|
||||
new Map<
|
||||
string,
|
||||
{
|
||||
deco: CategoryDecoration;
|
||||
values: CommandItem[];
|
||||
}
|
||||
>()
|
||||
);
|
||||
|
||||
function updateCommandList(rawCommandList: typeof props.commandList) {
|
||||
commandList.value.clear();
|
||||
for (const { category, values } of rawCommandList) {
|
||||
commandList.value.set(category, {
|
||||
deco: convCategoryDeco(category),
|
||||
values
|
||||
});
|
||||
}
|
||||
commandList.value.clear();
|
||||
for (const { category, values } of rawCommandList) {
|
||||
commandList.value.set(category, {
|
||||
deco: convCategoryDeco(category),
|
||||
values,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.commandList, updateCommandList);
|
||||
updateCommandList(props.commandList);
|
||||
|
||||
watch(chosenCategory, (category) => {
|
||||
const itemInfo = commandList.value?.get(category);
|
||||
if (itemInfo === undefined) {
|
||||
console.error(`category 없음: ${category}`);
|
||||
return;
|
||||
}
|
||||
chosenSubList.value = itemInfo.values;
|
||||
if (props.activatedCategory !== category) {
|
||||
emits('update:activatedCategory', category);
|
||||
}
|
||||
const itemInfo = commandList.value?.get(category);
|
||||
if (itemInfo === undefined) {
|
||||
console.error(`category 없음: ${category}`);
|
||||
return;
|
||||
}
|
||||
chosenSubList.value = itemInfo.values;
|
||||
if (props.activatedCategory !== category) {
|
||||
emits("update:activatedCategory", category);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (!categories.has(props.activatedCategory)) {
|
||||
chosenCategory.value = props.commandList[0].category;
|
||||
}
|
||||
else {
|
||||
chosenCategory.value = props.activatedCategory;
|
||||
}
|
||||
|
||||
if (!categories.has(props.activatedCategory)) {
|
||||
chosenCategory.value = props.commandList[0].category;
|
||||
} else {
|
||||
chosenCategory.value = props.activatedCategory;
|
||||
}
|
||||
});
|
||||
|
||||
function show(): void {
|
||||
showForm.value = true;
|
||||
showForm.value = true;
|
||||
}
|
||||
|
||||
function toggle(): void {
|
||||
showForm.value = !showForm.value;
|
||||
if (showForm.value === false) {
|
||||
emits('onClose');
|
||||
}
|
||||
showForm.value = !showForm.value;
|
||||
if (showForm.value === false) {
|
||||
emits("onClose");
|
||||
}
|
||||
}
|
||||
|
||||
function close(category?: string): void {
|
||||
showForm.value = false;
|
||||
emits('onClose', category);
|
||||
showForm.value = false;
|
||||
emits("onClose", category);
|
||||
}
|
||||
|
||||
const emits = defineEmits<{
|
||||
(event: 'onClose', command?: string): void,
|
||||
(event: 'update:activatedCategory', category: string): void,
|
||||
(event: "onClose", command?: string): void;
|
||||
(event: "update:activatedCategory", category: string): void;
|
||||
}>();
|
||||
|
||||
defineExpose({
|
||||
show,
|
||||
close,
|
||||
toggle
|
||||
})
|
||||
|
||||
show,
|
||||
close,
|
||||
toggle,
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.commandItem {
|
||||
border: gray 1px solid;
|
||||
border-radius: 0.5em;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 0.1em;
|
||||
margin: 0;
|
||||
border: gray 1px solid;
|
||||
border-radius: 0.5em;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
padding: 0.1em;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
userSelect: disabled ? undefined : 'none',
|
||||
overflow: 'hidden',
|
||||
touchAction: disabled ? undefined : 'none',
|
||||
}
|
||||
"
|
||||
}"
|
||||
:class="{ disabledDrag: disabled }"
|
||||
>
|
||||
<slot :selected="intersected" />
|
||||
@@ -16,14 +15,7 @@
|
||||
|
||||
<script lang="ts">
|
||||
/// https://github.com/andi23rosca/drag-select-vue/blob/master/src/DragSelect.vue
|
||||
import {
|
||||
defineComponent,
|
||||
ref,
|
||||
watch,
|
||||
onMounted,
|
||||
onBeforeUnmount,
|
||||
type PropType,
|
||||
} from "vue";
|
||||
import { defineComponent, ref, watch, onMounted, onBeforeUnmount, type PropType } from "vue";
|
||||
import VueTypes from "vue-types";
|
||||
|
||||
function getDimensions(p1: coord, p2: coord): rect {
|
||||
@@ -47,14 +39,8 @@ type rect = { width: number; height: number };
|
||||
export default defineComponent({
|
||||
props: {
|
||||
attribute: VueTypes.string.isRequired,
|
||||
color: {
|
||||
...VueTypes.string.def("#4299E1"),
|
||||
required: false,
|
||||
},
|
||||
opacity: {
|
||||
...VueTypes.number.def(0.7),
|
||||
required: false,
|
||||
},
|
||||
color: VueTypes.string.def("#4299E1"),
|
||||
opacity: VueTypes.number.def(0.7),
|
||||
modelValue: {
|
||||
type: Object as PropType<Set<string>>,
|
||||
required: false,
|
||||
@@ -64,7 +50,7 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue", "dragDone", "dragStart"],
|
||||
setup(props, { emit }) {
|
||||
@@ -194,20 +180,22 @@ export default defineComponent({
|
||||
isMine = false;
|
||||
}
|
||||
|
||||
watch(() => props.disabled, disabledNext => {
|
||||
if (disabledNext) {
|
||||
uContainer.removeEventListener("mousedown", startDrag);
|
||||
uContainer.removeEventListener("touchstart", touchStart);
|
||||
document.removeEventListener("mouseup", endDrag);
|
||||
document.removeEventListener("touchend", endDrag);
|
||||
watch(
|
||||
() => props.disabled,
|
||||
(disabledNext) => {
|
||||
if (disabledNext) {
|
||||
uContainer.removeEventListener("mousedown", startDrag);
|
||||
uContainer.removeEventListener("touchstart", touchStart);
|
||||
document.removeEventListener("mouseup", endDrag);
|
||||
document.removeEventListener("touchend", endDrag);
|
||||
} else {
|
||||
uContainer.addEventListener("mousedown", startDrag);
|
||||
uContainer.addEventListener("touchstart", touchStart);
|
||||
document.addEventListener("mouseup", endDrag);
|
||||
document.addEventListener("touchend", endDrag);
|
||||
}
|
||||
}
|
||||
else {
|
||||
uContainer.addEventListener("mousedown", startDrag);
|
||||
uContainer.addEventListener("touchstart", touchStart);
|
||||
document.addEventListener("mouseup", endDrag);
|
||||
document.addEventListener("touchend", endDrag);
|
||||
}
|
||||
});
|
||||
);
|
||||
|
||||
if (!props.disabled) {
|
||||
uContainer.addEventListener("mousedown", startDrag);
|
||||
@@ -216,7 +204,6 @@ export default defineComponent({
|
||||
document.addEventListener("touchend", endDrag);
|
||||
}
|
||||
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
uContainer.removeEventListener("mousedown", startDrag);
|
||||
uContainer.removeEventListener("touchstart", touchStart);
|
||||
@@ -231,4 +218,4 @@ export default defineComponent({
|
||||
};
|
||||
},
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
<template>
|
||||
<div
|
||||
:id="uuid"
|
||||
:class="['world_map', `map_theme_${mapTheme}`, 'draw_required']"
|
||||
>
|
||||
<div :id="uuid" :class="['world_map', `map_theme_${mapName}`, 'draw_required']">
|
||||
<div
|
||||
class="map_title obj_tooltip"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
data-tooltip-class="map_title_tooltiptext"
|
||||
>
|
||||
<span class="map_title_text"> </span>
|
||||
<span class="tooltiptext"></span>
|
||||
<span class="map_title_text" />
|
||||
<span class="tooltiptext" />
|
||||
</div>
|
||||
<div class="map_body">
|
||||
<div class="map_bglayer1"></div>
|
||||
<div class="map_bglayer2"></div>
|
||||
<div class="map_bgroad"></div>
|
||||
<div class="map_bglayer1" />
|
||||
<div class="map_bglayer2" />
|
||||
<div class="map_bgroad" />
|
||||
<div class="map_button_stack">
|
||||
<button
|
||||
type="button"
|
||||
@@ -38,8 +35,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="city_tooltip">
|
||||
<div class="city_name"></div>
|
||||
<div class="nation_name"></div>
|
||||
<div class="city_name" />
|
||||
<div class="nation_name" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -51,7 +48,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
export type { MapCityParsed };
|
||||
export default defineComponent({
|
||||
props: {
|
||||
mapTheme: {
|
||||
mapName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
@@ -59,6 +56,7 @@ export default defineComponent({
|
||||
clickableAll: { type: Boolean, default: undefined, required: false },
|
||||
selectCallback: {
|
||||
type: Function as PropType<loadMapOption["selectCallback"]>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
hrefTemplate: { type: String, default: undefined, required: false },
|
||||
@@ -88,6 +86,7 @@ export default defineComponent({
|
||||
|
||||
modelValue: {
|
||||
type: Object as PropType<MapCityParsed>,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<template>
|
||||
<div class="row form-group number-input-with-info">
|
||||
<label v-if="!right" class="col-6 col-form-label ">{{ title }}</label>
|
||||
<label v-if="!right" class="col-6 col-form-label">{{ title }}</label>
|
||||
<div class="col-6">
|
||||
<input
|
||||
ref="input"
|
||||
v-model="rawValue"
|
||||
type="number"
|
||||
:step="step ?? undefined"
|
||||
v-model="rawValue"
|
||||
class="form-control f_tnum"
|
||||
:min="min ?? undefined"
|
||||
:max="max ?? undefined"
|
||||
:style="{ display: editmode ? undefined : 'none' }"
|
||||
@blur="onBlurNumber"
|
||||
@input="updateValue"
|
||||
:style="{ display: editmode ? undefined : 'none' }"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
class="form-control f_tnum"
|
||||
:readonly="readonly"
|
||||
:value="printValue"
|
||||
@focus="onFocusText"
|
||||
:style="{ display: !editmode ? undefined : 'none' }"
|
||||
@focus="onFocusText"
|
||||
/>
|
||||
</div>
|
||||
<label v-if="right" class="col-6 col-form-label">{{ title }}</label>
|
||||
</div>
|
||||
<div style="text-align: right">
|
||||
<small class="form-text text-muted"><slot></slot></small>
|
||||
<small class="form-text text-muted"><slot /></small>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
@@ -56,10 +56,12 @@ export default defineComponent({
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
step: {
|
||||
type: Number,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
@@ -70,7 +72,7 @@ export default defineComponent({
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
}
|
||||
},
|
||||
},
|
||||
emits: ["update:modelValue"],
|
||||
data() {
|
||||
@@ -80,15 +82,15 @@ export default defineComponent({
|
||||
printValue: this.modelValue.toLocaleString(),
|
||||
};
|
||||
},
|
||||
watch:{
|
||||
modelValue: function(newVal:number){
|
||||
watch: {
|
||||
modelValue: function (newVal: number) {
|
||||
this.rawValue = newVal;
|
||||
this.printValue = newVal.toLocaleString();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
updateValue() {
|
||||
if(this.readonly){
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
if (this.int) {
|
||||
@@ -102,7 +104,7 @@ export default defineComponent({
|
||||
this.printValue = this.rawValue.toLocaleString();
|
||||
},
|
||||
onFocusText() {
|
||||
if(this.readonly){
|
||||
if (this.readonly) {
|
||||
return;
|
||||
}
|
||||
this.editmode = true;
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
<template>
|
||||
<span class="time-zone">{{serverNow}}</span>
|
||||
<span class="time-zone">{{ serverNow }}</span>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { addMilliseconds } from 'date-fns';
|
||||
import { type PropType, ref, onMounted, watch } from 'vue';
|
||||
import { formatTime } from '@/util/formatTime';
|
||||
import { addMilliseconds } from "date-fns";
|
||||
import { type PropType, ref, onMounted, watch } from "vue";
|
||||
import { formatTime } from "@/util/formatTime";
|
||||
const props = defineProps({
|
||||
serverTime: {
|
||||
type: Object as PropType<Date>,
|
||||
required: false,
|
||||
default: new Date(),
|
||||
},
|
||||
timeFormat: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'HH:mm:ss'
|
||||
}
|
||||
})
|
||||
serverTime: {
|
||||
type: Object as PropType<Date>,
|
||||
required: false,
|
||||
default: new Date(),
|
||||
},
|
||||
timeFormat: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: "HH:mm:ss",
|
||||
},
|
||||
});
|
||||
|
||||
const timeDiff = ref(0);
|
||||
const serverNow = ref('');
|
||||
const serverNow = ref("");
|
||||
|
||||
watch(()=>props.serverTime, (newValue)=>{
|
||||
watch(
|
||||
() => props.serverTime,
|
||||
(newValue) => {
|
||||
const clientNow = new Date();
|
||||
timeDiff.value = newValue.getTime() - clientNow.getTime();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
function updateNow() {
|
||||
const serverNowObj = addMilliseconds(new Date(), timeDiff.value);
|
||||
@@ -36,8 +39,8 @@ function updateNow() {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const clientNow = new Date();
|
||||
timeDiff.value = props.serverTime.getTime() - clientNow.getTime();
|
||||
updateNow();
|
||||
const clientNow = new Date();
|
||||
timeDiff.value = props.serverTime.getTime() - clientNow.getTime();
|
||||
updateNow();
|
||||
});
|
||||
</script>
|
||||
</script>
|
||||
|
||||
+326
-346
@@ -1,53 +1,54 @@
|
||||
<template>
|
||||
<b-button-toolbar key-nav v-if="editable && editor" class="bg-dark">
|
||||
<b-button-group class="mx-1">
|
||||
<b-button
|
||||
@click="editor.commands.undo()"
|
||||
<BButtonToolbar v-if="editable && editor" key-nav class="bg-dark">
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton v-b-tooltip.hover title="되돌리기" @click="editor?.commands.undo()">
|
||||
<i class="bi bi-arrow-90deg-left" />
|
||||
</BButton>
|
||||
<BButton v-b-tooltip.hover title="재실행" @click="editor?.commands.redo()">
|
||||
<i class="bi bi-arrow-90deg-right" />
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
title="되돌리기"
|
||||
><i class="bi bi-arrow-90deg-left"></i
|
||||
></b-button>
|
||||
<b-button @click="editor.commands.redo()" v-b-tooltip.hover title="재실행"
|
||||
><i class="bi bi-arrow-90deg-right"></i
|
||||
></b-button>
|
||||
</b-button-group>
|
||||
<b-button-group class="mx-1">
|
||||
<b-button
|
||||
@click="editor.chain().focus().toggleBold().run()"
|
||||
:class="{ 'is-active': editor.isActive('bold') }"
|
||||
v-b-tooltip.hover
|
||||
title="진하게"
|
||||
><i class="bi bi-type-bold"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="editor.chain().focus().toggleItalic().run()"
|
||||
@click="editor?.chain().focus().toggleBold().run()"
|
||||
>
|
||||
<i class="bi bi-type-bold" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive('italic') }"
|
||||
v-b-tooltip.hover
|
||||
title="기울이기"
|
||||
><i class="bi bi-type-italic"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="editor.chain().focus().toggleUnderline().run()"
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
@click="editor?.chain().focus().toggleItalic().run()"
|
||||
>
|
||||
<i class="bi bi-type-italic" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive('underline') }"
|
||||
title="밑줄"
|
||||
><i class="bi bi-type-underline"></i
|
||||
></b-button>
|
||||
@click="editor?.chain().focus().toggleUnderline().run()"
|
||||
>
|
||||
<i class="bi bi-type-underline" />
|
||||
</BButton>
|
||||
<!-- 효과 지우기 -->
|
||||
</b-button-group>
|
||||
</BButtonGroup>
|
||||
|
||||
<b-button-group class="mx-1">
|
||||
<b-dropdown>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BDropdown>
|
||||
<template #button-content> 크기 </template>
|
||||
<b-dropdown-item @click="editor.chain().focus().unsetFontSize().run()"
|
||||
><span>기본</span></b-dropdown-item
|
||||
>
|
||||
<b-dropdown-divider />
|
||||
<b-dropdown-item
|
||||
<BDropdownItem @click="editor?.chain().focus().unsetFontSize().run()">
|
||||
<span>기본</span>
|
||||
</BDropdownItem>
|
||||
<BDropdownDivider />
|
||||
<BDropdownItem
|
||||
v-for="sizeItem in fontSize"
|
||||
:key="sizeItem"
|
||||
@click="editor.chain().focus().setFontSize(sizeItem).run()"
|
||||
><span
|
||||
@click="editor?.chain().focus().setFontSize(sizeItem).run()"
|
||||
>
|
||||
<span
|
||||
:style="{
|
||||
'font-size': sizeItem,
|
||||
'text-decoration': editor.isActive('textStyle', {
|
||||
@@ -57,235 +58,230 @@
|
||||
: undefined,
|
||||
}"
|
||||
>{{ sizeItem }}</span
|
||||
></b-dropdown-item
|
||||
>
|
||||
</b-dropdown>
|
||||
>
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
<!-- 글꼴 -->
|
||||
</b-button-group>
|
||||
</BButtonGroup>
|
||||
|
||||
<b-button-group class="mx-1">
|
||||
<b-button
|
||||
@click="editor.chain().focus().toggleStrike().run()"
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive('strike') }"
|
||||
title="가로선"
|
||||
><i class="bi bi-type-strikethrough"></i
|
||||
></b-button>
|
||||
@click="editor?.chain().focus().toggleStrike().run()"
|
||||
>
|
||||
<i class="bi bi-type-strikethrough" />
|
||||
</BButton>
|
||||
<!-- 윗첨자, 아랫첨자 -->
|
||||
</b-button-group>
|
||||
</BButtonGroup>
|
||||
|
||||
<b-button-group class="mx-1">
|
||||
<b-button
|
||||
@click="
|
||||
editor.chain().focus().unsetColor().unsetBackgroundColor().run()
|
||||
"
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
title="색상 취소"
|
||||
><i class="bi bi-droplet"></i
|
||||
></b-button>
|
||||
@click="editor?.chain().focus().unsetColor().unsetBackgroundColor().run()"
|
||||
>
|
||||
<i class="bi bi-droplet" />
|
||||
</BButton>
|
||||
<input
|
||||
v-b-tooltip.hover
|
||||
type="color"
|
||||
class="form-control form-control-color"
|
||||
:value="
|
||||
colorConvert(editor.getAttributes('textStyle').color, '#ffffff')
|
||||
"
|
||||
@input="editor.chain().focus().setColor(($event.target as HTMLInputElement).value).run()"
|
||||
v-b-tooltip.hover
|
||||
:value="colorConvert(editor.getAttributes('textStyle').color, '#ffffff')"
|
||||
title="글자색"
|
||||
@input="editor?.chain().focus().setColor(($event.target as HTMLInputElement).value).run()"
|
||||
/>
|
||||
<input
|
||||
v-b-tooltip.hover
|
||||
type="color"
|
||||
class="form-control form-control-color"
|
||||
:value="
|
||||
colorConvert(
|
||||
editor.getAttributes('textStyle').backgroundColor,
|
||||
'#000000'
|
||||
)
|
||||
"
|
||||
@input="
|
||||
editor.chain().focus().setBackgroundColor(($event.target as HTMLInputElement).value).run()
|
||||
"
|
||||
v-b-tooltip.hover
|
||||
:value="colorConvert(editor.getAttributes('textStyle').backgroundColor, '#000000')"
|
||||
title="배경색"
|
||||
@input="
|
||||
editor?.chain().focus().setBackgroundColor(($event.target as HTMLInputElement).value).run()
|
||||
"
|
||||
/>
|
||||
</b-button-group>
|
||||
</BButtonGroup>
|
||||
|
||||
<b-button-group class="mx-1">
|
||||
<b-button
|
||||
v-b-tooltip.hover
|
||||
@click="showImageModal = true"
|
||||
title="이미지 추가"
|
||||
><i class="bi bi-image"></i
|
||||
></b-button>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton v-b-tooltip.hover title="이미지 추가" @click="showImageModal = true">
|
||||
<i class="bi bi-image" />
|
||||
</BButton>
|
||||
<!-- 이미지추가 -->
|
||||
<!-- 링크 -->
|
||||
<!-- 영상링크 -->
|
||||
<!-- 표 -->
|
||||
<!-- 구분선 삽입 -->
|
||||
<b-button
|
||||
@click="editor.chain().focus().setHorizontalRule().run()"
|
||||
v-b-tooltip.hover
|
||||
title="구분선"
|
||||
><i class="bi bi-hr"></i
|
||||
></b-button>
|
||||
</b-button-group>
|
||||
<BButton v-b-tooltip.hover title="구분선" @click="editor?.chain().focus().setHorizontalRule().run()">
|
||||
<i class="bi bi-hr" />
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
|
||||
<b-button-group class="mx-1">
|
||||
<BButtonGroup class="mx-1">
|
||||
<!-- 글머리 기호 -->
|
||||
<!-- 번호 매기기 -->
|
||||
<b-button
|
||||
@click="editor.chain().focus().setTextAlign('left').run()"
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'left' }) }"
|
||||
v-b-tooltip.hover
|
||||
title="왼쪽 정렬"
|
||||
><i class="bi bi-text-left"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setTextAlign('center').run()"
|
||||
@click="editor?.chain().focus().setTextAlign('left').run()"
|
||||
>
|
||||
<i class="bi bi-text-left" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'center' }) }"
|
||||
v-b-tooltip.hover
|
||||
title="가운데 정렬"
|
||||
><i class="bi bi-text-center"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setTextAlign('right').run()"
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
|
||||
@click="editor?.chain().focus().setTextAlign('center').run()"
|
||||
>
|
||||
<i class="bi bi-text-center" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{ 'is-active': editor.isActive({ textAlign: 'right' }) }"
|
||||
title="오른쪽 정렬"
|
||||
><i class="bi bi-text-right"></i
|
||||
></b-button>
|
||||
@click="editor?.chain().focus().setTextAlign('right').run()"
|
||||
>
|
||||
<i class="bi bi-text-right" />
|
||||
</BButton>
|
||||
<!-- 문단정렬(왼, 가, 오, 양)(내어, 들여) -->
|
||||
</b-button-group>
|
||||
</BButtonGroup>
|
||||
|
||||
<b-button-group class="mx-1"> </b-button-group>
|
||||
<BButtonGroup class="mx-1" />
|
||||
|
||||
<b-button-group class="mx-1">
|
||||
<BButtonGroup class="mx-1">
|
||||
<!-- 줄간격 (1.0, 1.2, 1.4, 1.5, 1.6, 1.8, 2.0, 3.0) -->
|
||||
</b-button-group>
|
||||
</BButtonGroup>
|
||||
|
||||
<b-button-group class="mx-1">
|
||||
<BButtonGroup class="mx-1">
|
||||
<!-- 원본 코드 -->
|
||||
</b-button-group>
|
||||
</b-button-toolbar>
|
||||
<bubble-menu
|
||||
:tippy-options="{ animation: false, maxWidth: 600 }"
|
||||
:editor="editor"
|
||||
</BButtonGroup>
|
||||
</BButtonToolbar>
|
||||
<BubbleMenu
|
||||
v-if="editable && editor"
|
||||
v-show="editor.isActive('custom-image')"
|
||||
:tippyOptions="{ animation: false, maxWidth: 600 }"
|
||||
:editor="editor"
|
||||
>
|
||||
<b-button-toolbar>
|
||||
<b-button-group class="mx-1">
|
||||
<b-button
|
||||
@click="editor.chain().focus().setImageEx({ size: 'small' }).run()"
|
||||
<BButtonToolbar>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'small',
|
||||
}),
|
||||
f_frac: true,
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="1/4 너비로 채우기"
|
||||
>1/4</b-button
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'small' }).run()"
|
||||
>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setImageEx({ size: 'medium' }).run()"
|
||||
1/4
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'medium',
|
||||
}),
|
||||
f_frac: true,
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="1/2 너비로 채우기"
|
||||
>1/2</b-button
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'medium' }).run()"
|
||||
>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setImageEx({ size: 'large' }).run()"
|
||||
1/2
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'large',
|
||||
}),
|
||||
f_frac: true,
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="가득 채우기"
|
||||
>1</b-button
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'large' }).run()"
|
||||
>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setImageEx({ size: 'original' }).run()"
|
||||
1
|
||||
</BButton>
|
||||
<BButton
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
size: 'original',
|
||||
}),
|
||||
}"
|
||||
>원본</b-button
|
||||
@click="editor?.chain().focus().setImageEx({ size: 'original' }).run()"
|
||||
>
|
||||
</b-button-group>
|
||||
<b-button-group class="mx-1">
|
||||
<b-button
|
||||
@click="
|
||||
editor.chain().focus().setImageEx({ align: 'float-left' }).run()
|
||||
"
|
||||
원본
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
<BButtonGroup class="mx-1">
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'float-left',
|
||||
}),
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="왼쪽으로 붙이기"
|
||||
><i class="bi bi-chevron-bar-left"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setImageEx({ align: 'left' }).run()"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'float-left' }).run()"
|
||||
>
|
||||
<i class="bi bi-chevron-bar-left" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'left',
|
||||
}),
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="왼쪽으로"
|
||||
><i class="bi bi-align-start"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setImageEx({ align: 'center' }).run()"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'left' }).run()"
|
||||
>
|
||||
<i class="bi bi-align-start" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'center',
|
||||
}),
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="가운데로"
|
||||
><i class="bi bi-align-center"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="editor.chain().focus().setImageEx({ align: 'right' }).run()"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'center' }).run()"
|
||||
>
|
||||
<i class="bi bi-align-center" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'right',
|
||||
}),
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="오른쪽으로 붙이기"
|
||||
><i class="bi bi-align-end"></i
|
||||
></b-button>
|
||||
<b-button
|
||||
@click="
|
||||
editor.chain().focus().setImageEx({ align: 'float-right' }).run()
|
||||
"
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'right' }).run()"
|
||||
>
|
||||
<i class="bi bi-align-end" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip.hover
|
||||
:class="{
|
||||
'is-active': editor.isActive('custom-image', {
|
||||
float: 'float-right',
|
||||
}),
|
||||
}"
|
||||
v-b-tooltip.hover
|
||||
title="오른쪽으로 붙이기"
|
||||
><i class="bi bi-chevron-bar-right"></i
|
||||
></b-button>
|
||||
</b-button-group>
|
||||
</b-button-toolbar>
|
||||
</bubble-menu>
|
||||
<editor-content :editor="editor" class="tiptap-editor" />
|
||||
<b-modal
|
||||
@click="editor?.chain().focus().setImageEx({ align: 'float-right' }).run()"
|
||||
>
|
||||
<i class="bi bi-chevron-bar-right" />
|
||||
</BButton>
|
||||
</BButtonGroup>
|
||||
</BButtonToolbar>
|
||||
</BubbleMenu>
|
||||
<EditorContent :editor="editor" class="tiptap-editor" />
|
||||
<BModal
|
||||
v-model="showImageModal"
|
||||
title="이미지 추가"
|
||||
okTitle="추가"
|
||||
@@ -295,7 +291,7 @@
|
||||
@hidden="resetModal"
|
||||
>
|
||||
<div class="bg-light text-dark">
|
||||
<b-form-group
|
||||
<BFormGroup
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
content-cols-sm
|
||||
@@ -306,14 +302,14 @@
|
||||
:label-for="`${uuid}_image_upload`"
|
||||
>
|
||||
<input
|
||||
:id="`${uuid}_image_upload`"
|
||||
class="form-control"
|
||||
type="file"
|
||||
:id="`${uuid}_image_upload`"
|
||||
@change="chooseImage"
|
||||
accept=".jpg,.jpeg,.png,.gif,.webp"
|
||||
@change="chooseImage"
|
||||
/>
|
||||
</b-form-group>
|
||||
<b-form-group
|
||||
</BFormGroup>
|
||||
<BFormGroup
|
||||
label-cols-sm="4"
|
||||
label-cols-lg="3"
|
||||
content-cols-sm
|
||||
@@ -323,15 +319,15 @@
|
||||
label-align="right"
|
||||
:label-for="`${uuid}_image_link`"
|
||||
>
|
||||
<b-form-input v-model="imageLink"></b-form-input>
|
||||
</b-form-group>
|
||||
<BFormInput v-model="imageLink" />
|
||||
</BFormGroup>
|
||||
</div>
|
||||
</b-modal>
|
||||
</BModal>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
//import "@scss/common/bootstrap5.scss";
|
||||
import { defineComponent } from "vue";
|
||||
import { onBeforeUnmount, onMounted, ref, watch } from "vue";
|
||||
import { Editor, EditorContent, BubbleMenu } from "@tiptap/vue-3";
|
||||
import { FontSize } from "@/tiptap-ext/FontSize";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
@@ -351,6 +347,8 @@ import {
|
||||
BDropdownItem,
|
||||
BDropdownDivider,
|
||||
BModal,
|
||||
BFormGroup,
|
||||
BFormInput,
|
||||
} from "bootstrap-vue-3";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { unwrap } from "@/util/unwrap";
|
||||
@@ -359,178 +357,160 @@ import { isObject, isString } from "lodash";
|
||||
import type { AxiosError } from "axios";
|
||||
import { SammoAPI } from "@/SammoAPI";
|
||||
|
||||
const compoment = defineComponent({
|
||||
components: {
|
||||
EditorContent,
|
||||
BubbleMenu,
|
||||
BModal,
|
||||
BButtonGroup,
|
||||
BButtonToolbar,
|
||||
BButton,
|
||||
BDropdown,
|
||||
BDropdownItem,
|
||||
BDropdownDivider,
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
emits: ["ready", "update:modelValue"],
|
||||
methods: {
|
||||
unwrap,
|
||||
chooseImage(e: Event) {
|
||||
const target = unwrap(e.target) as HTMLInputElement;
|
||||
this.imageUploadFiles = target.files;
|
||||
},
|
||||
colorConvert(val: string | undefined, defaultVal: string) {
|
||||
if (!val) {
|
||||
return defaultVal;
|
||||
}
|
||||
if (val.startsWith("rgb")) {
|
||||
const rgb = val.split("(")[1].split(")")[0].split(",");
|
||||
const vals: string[] = [];
|
||||
for (const subColor of rgb) {
|
||||
const hexSubColor = parseInt(subColor).toString(16);
|
||||
if (hexSubColor.length == 1) {
|
||||
vals.push("0");
|
||||
}
|
||||
vals.push(hexSubColor);
|
||||
}
|
||||
return `#${vals.join("")}`;
|
||||
}
|
||||
return val;
|
||||
},
|
||||
async tryAddImage(bvModalEvt: Event) {
|
||||
if (this.imageUploadFiles === null || this.imageUploadFiles.length == 0) {
|
||||
this.addImageLink(bvModalEvt);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetImage = unwrap(this.imageUploadFiles.item(0));
|
||||
let imageResult: {
|
||||
result: true;
|
||||
path: string;
|
||||
};
|
||||
try {
|
||||
const base64Binary = await getBase64FromFileObject(targetImage);
|
||||
imageResult = await SammoAPI.Misc.UploadImage({
|
||||
imageData: base64Binary,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
bvModalEvt.preventDefault();
|
||||
}
|
||||
|
||||
if (isObject(e) && "response" in e) {
|
||||
const axiosErr = e as AxiosError;
|
||||
if (axiosErr.response?.status === 413) {
|
||||
alert("허용 용량을 초과했습니다.");
|
||||
bvModalEvt.preventDefault();
|
||||
}
|
||||
}
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
const imagePath = imageResult.path;
|
||||
this.editor.chain().focus().setImageEx({ src: imagePath }).run();
|
||||
},
|
||||
addImageLink(bvModalEvt: Event) {
|
||||
if (!this.imageLink) {
|
||||
alert("업로드할 이미지를 선택하거나, 이미지 주소를 입력해주세요.");
|
||||
bvModalEvt.preventDefault();
|
||||
return false;
|
||||
}
|
||||
this.editor.chain().focus().setImageEx({ src: this.imageLink }).run();
|
||||
},
|
||||
resetModal() {
|
||||
this.imageLink = "";
|
||||
this.imageUploadFiles = null;
|
||||
},
|
||||
},
|
||||
|
||||
props: {
|
||||
modelValue: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
uuid: uuidv4(),
|
||||
editor: null as unknown as InstanceType<typeof Editor>,
|
||||
fontList: ["Pretendard", "맑은 고딕", "궁서", "돋움"],
|
||||
fontSize: [
|
||||
"8px",
|
||||
"10px",
|
||||
"12px",
|
||||
"14px",
|
||||
"18px",
|
||||
"22px",
|
||||
"28px",
|
||||
"36px",
|
||||
"48px",
|
||||
"72px",
|
||||
],
|
||||
imageUploadFiles: null as FileList | null,
|
||||
imageLink: "",
|
||||
showImageModal: false,
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
modelValue(value: string) {
|
||||
const isSame = this.editor.getHTML() === value;
|
||||
|
||||
if (isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.editor.commands.setContent(value, false);
|
||||
},
|
||||
editable(value: boolean) {
|
||||
this.editor.options.editable = value;
|
||||
if (value == true) {
|
||||
this.editor.commands.focus();
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
const editor = new Editor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Underline,
|
||||
FontSize,
|
||||
TextStyle,
|
||||
TextAlign.configure({
|
||||
types: ["heading", "paragraph"],
|
||||
}),
|
||||
Color.configure({
|
||||
types: ["textStyle"],
|
||||
}),
|
||||
BackgroundColor.configure({
|
||||
types: ["textStyle"],
|
||||
}),
|
||||
CustomImage,
|
||||
Link,
|
||||
],
|
||||
editable: this.editable,
|
||||
content: this.modelValue,
|
||||
onUpdate: () => {
|
||||
this.$emit("update:modelValue", this.editor.getHTML());
|
||||
},
|
||||
onCreate: () => {
|
||||
this.$emit("ready");
|
||||
}
|
||||
});
|
||||
this.editor = editor;
|
||||
},
|
||||
|
||||
beforeUnmount() {
|
||||
this.editor.destroy();
|
||||
editable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
});
|
||||
export default compoment;
|
||||
</script>
|
||||
|
||||
const emit = defineEmits(["ready", "update:modelValue"]);
|
||||
|
||||
const uuid = ref(uuidv4());
|
||||
const editor = ref<InstanceType<typeof Editor>>();
|
||||
//const fontList = ref(["Pretendard", "맑은 고딕", "궁서", "돋움"]);
|
||||
const fontSize = ref(["8px", "10px", "12px", "14px", "18px", "22px", "28px", "36px", "48px", "72px"]);
|
||||
const imageUploadFiles = ref(null as FileList | null);
|
||||
const imageLink = ref("");
|
||||
const showImageModal = ref(false);
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value: string) => {
|
||||
const isSame = editor.value?.getHTML() === value;
|
||||
|
||||
if (isSame) {
|
||||
return;
|
||||
}
|
||||
|
||||
editor.value?.commands.setContent(value, false);
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.editable,
|
||||
(value: boolean) => {
|
||||
if (!editor.value) {
|
||||
return;
|
||||
}
|
||||
editor.value.options.editable = value;
|
||||
if (value == true) {
|
||||
editor.value.commands.focus();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
const vEditor = new Editor({
|
||||
extensions: [
|
||||
StarterKit,
|
||||
Underline,
|
||||
FontSize,
|
||||
TextStyle,
|
||||
TextAlign.configure({
|
||||
types: ["heading", "paragraph"],
|
||||
}),
|
||||
Color.configure({
|
||||
types: ["textStyle"],
|
||||
}),
|
||||
BackgroundColor.configure({
|
||||
types: ["textStyle"],
|
||||
}),
|
||||
CustomImage,
|
||||
Link,
|
||||
],
|
||||
editable: props.editable,
|
||||
content: props.modelValue,
|
||||
onUpdate: () => {
|
||||
emit("update:modelValue", editor.value?.getHTML());
|
||||
},
|
||||
onCreate: () => {
|
||||
emit("ready");
|
||||
},
|
||||
});
|
||||
editor.value = vEditor;
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
editor.value?.destroy();
|
||||
});
|
||||
|
||||
function chooseImage(e: Event) {
|
||||
const target = unwrap(e.target) as HTMLInputElement;
|
||||
imageUploadFiles.value = target.files;
|
||||
}
|
||||
|
||||
function colorConvert(val: string | undefined, defaultVal: string) {
|
||||
if (!val) {
|
||||
return defaultVal;
|
||||
}
|
||||
if (val.startsWith("rgb")) {
|
||||
const rgb = val.split("(")[1].split(")")[0].split(",");
|
||||
const vals: string[] = [];
|
||||
for (const subColor of rgb) {
|
||||
const hexSubColor = parseInt(subColor).toString(16);
|
||||
if (hexSubColor.length == 1) {
|
||||
vals.push("0");
|
||||
}
|
||||
vals.push(hexSubColor);
|
||||
}
|
||||
return `#${vals.join("")}`;
|
||||
}
|
||||
return val;
|
||||
}
|
||||
|
||||
async function tryAddImage(bvModalEvt: Event) {
|
||||
if (imageUploadFiles.value === null || imageUploadFiles.value.length == 0) {
|
||||
addImageLink(bvModalEvt);
|
||||
return;
|
||||
}
|
||||
|
||||
const targetImage = unwrap(unwrap(imageUploadFiles.value).item(0));
|
||||
let imageResult: {
|
||||
result: true;
|
||||
path: string;
|
||||
};
|
||||
try {
|
||||
const base64Binary = await getBase64FromFileObject(targetImage);
|
||||
imageResult = await SammoAPI.Misc.UploadImage({
|
||||
imageData: base64Binary,
|
||||
});
|
||||
} catch (e) {
|
||||
if (isString(e)) {
|
||||
alert(e);
|
||||
bvModalEvt.preventDefault();
|
||||
}
|
||||
|
||||
if (isObject(e) && "response" in e) {
|
||||
const axiosErr = e as AxiosError;
|
||||
if (axiosErr.response?.status === 413) {
|
||||
alert("허용 용량을 초과했습니다.");
|
||||
bvModalEvt.preventDefault();
|
||||
}
|
||||
}
|
||||
console.error(e);
|
||||
return false;
|
||||
}
|
||||
|
||||
const imagePath = imageResult.path;
|
||||
editor.value?.chain().focus().setImageEx({ src: imagePath }).run();
|
||||
}
|
||||
|
||||
function addImageLink(bvModalEvt: Event) {
|
||||
if (!imageLink.value) {
|
||||
alert("업로드할 이미지를 선택하거나, 이미지 주소를 입력해주세요.");
|
||||
bvModalEvt.preventDefault();
|
||||
return false;
|
||||
}
|
||||
editor.value?.chain().focus().setImageEx({ src: imageLink.value }).run();
|
||||
}
|
||||
|
||||
function resetModal() {
|
||||
imageLink.value = "";
|
||||
imageUploadFiles.value = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -1,85 +1,70 @@
|
||||
<template>
|
||||
<div class="bg0 back_bar">
|
||||
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">
|
||||
돌아가기</button
|
||||
><button
|
||||
type="button"
|
||||
v-if="reloadable"
|
||||
class="btn btn-sammo-base2 reload_btn"
|
||||
@click="reload"
|
||||
>
|
||||
갱신
|
||||
</button>
|
||||
<div v-else></div>
|
||||
<h2 class="title">{{ title }}</h2>
|
||||
<button type="button" class="btn btn-sammo-base2 back_btn" @click="back">돌아가기</button
|
||||
><button v-if="reloadable" type="button" class="btn btn-sammo-base2 reload_btn" @click="reload">갱신</button>
|
||||
<div v-else />
|
||||
<h2 class="title">
|
||||
{{ title }}
|
||||
</h2>
|
||||
<div> </div>
|
||||
<b-button
|
||||
v-if="toggleSearch !== undefined"
|
||||
class="btn-toggle-zoom"
|
||||
:variant="toggleSearch ? 'info' : 'secondary'"
|
||||
:pressed="toggleSearch"
|
||||
v-if="toggleSearch !== undefined"
|
||||
@click="toggleSearch = !toggleSearch"
|
||||
>{{ toggleSearch ? "검색 켜짐" : "검색 꺼짐" }}</b-button
|
||||
>
|
||||
{{ toggleSearch ? "검색 켜짐" : "검색 꺼짐" }}
|
||||
</b-button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
<script lang="ts" setup>
|
||||
import "@scss/game_bg.scss";
|
||||
import { defineComponent, type PropType } from "vue";
|
||||
import { type PropType, ref, watch } from "vue";
|
||||
import VueTypes from "vue-types";
|
||||
|
||||
export default defineComponent({
|
||||
name: "TopBackBar",
|
||||
methods: {
|
||||
back() {
|
||||
if (this.type === "normal") {
|
||||
location.href = "./";
|
||||
} else if (this.type == "chief") {
|
||||
location.href = "v_chiefCenter.php";
|
||||
} else {
|
||||
//TODO: window.close하려면 부모창이 있어야함!
|
||||
window.close();
|
||||
}
|
||||
},
|
||||
reload() {
|
||||
this.$emit("reload");
|
||||
},
|
||||
const props = defineProps({
|
||||
title: VueTypes.string.isRequired,
|
||||
type: {
|
||||
type: String as PropType<"normal" | "chief" | "close">,
|
||||
default: "normal",
|
||||
required: false,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
toggleSearch: this.searchable,
|
||||
};
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
emits: ["update:searchable", "reload"],
|
||||
watch: {
|
||||
toggleSearch(val: boolean) {
|
||||
this.$emit("update:searchable", val);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<"normal" | "chief" | "close">,
|
||||
default: "normal",
|
||||
required: false,
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
reloadable: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
reloadable: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
required: false,
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
const emit = defineEmits(["update:searchable", "reload"]);
|
||||
|
||||
const toggleSearch = ref(props.searchable);
|
||||
|
||||
watch(toggleSearch, (val) => {
|
||||
emit("update:searchable", val);
|
||||
});
|
||||
|
||||
function back() {
|
||||
if (props.type === "normal") {
|
||||
location.href = "./";
|
||||
} else if (props.type == "chief") {
|
||||
location.href = "v_chiefCenter.php";
|
||||
} else {
|
||||
//TODO: window.close하려면 부모창이 있어야함!
|
||||
window.close();
|
||||
}
|
||||
}
|
||||
function reload() {
|
||||
emit("reload");
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.back_bar {
|
||||
@@ -112,4 +97,4 @@ export default defineComponent({
|
||||
font-size: 18pt;
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user