릴리스: v0.1.13 관리자 탭과 가변 티어 행 추가
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { computed, nextTick, onMounted, ref } from 'vue'
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import Sortable from 'sortablejs'
|
||||
import * as htmlToImage from 'html-to-image'
|
||||
@@ -38,12 +38,18 @@ const groupListEl = ref(null)
|
||||
const poolEl = ref(null)
|
||||
const groupDropEls = ref({})
|
||||
const fileEl = ref(null)
|
||||
const groupSortable = ref(null)
|
||||
const poolSortable = ref(null)
|
||||
const dropSortables = ref([])
|
||||
|
||||
const isNewTierList = computed(() => tierListId.value === 'new')
|
||||
const canEdit = computed(() => !!auth.user && (!ownerId.value || ownerId.value === auth.user.id))
|
||||
|
||||
function setGroupDropEl(groupId, el) {
|
||||
if (!el) return
|
||||
if (!el) {
|
||||
delete groupDropEls.value[groupId]
|
||||
return
|
||||
}
|
||||
groupDropEls.value[groupId] = el
|
||||
}
|
||||
|
||||
@@ -75,7 +81,9 @@ function resolveItemSrc(item) {
|
||||
async function initSortables() {
|
||||
if (!poolEl.value || !groupListEl.value) return
|
||||
|
||||
Sortable.create(groupListEl.value, {
|
||||
destroySortables()
|
||||
|
||||
groupSortable.value = Sortable.create(groupListEl.value, {
|
||||
animation: 160,
|
||||
handle: '[data-group-handle]',
|
||||
ghostClass: 'ghost',
|
||||
@@ -88,7 +96,7 @@ async function initSortables() {
|
||||
},
|
||||
})
|
||||
|
||||
Sortable.create(poolEl.value, {
|
||||
poolSortable.value = Sortable.create(poolEl.value, {
|
||||
group: 'tier-items',
|
||||
animation: 160,
|
||||
draggable: '[data-item-id]',
|
||||
@@ -98,7 +106,7 @@ async function initSortables() {
|
||||
onAdd: () => normalizeSort(poolEl.value),
|
||||
})
|
||||
|
||||
Object.entries(groupDropEls.value).forEach(([gid, el]) => {
|
||||
dropSortables.value = Object.entries(groupDropEls.value).map(([gid, el]) =>
|
||||
Sortable.create(el, {
|
||||
group: 'tier-items',
|
||||
animation: 160,
|
||||
@@ -108,7 +116,56 @@ async function initSortables() {
|
||||
onSort: () => normalizeSort(el),
|
||||
onAdd: () => normalizeSort(el),
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function destroySortables() {
|
||||
if (groupSortable.value) {
|
||||
groupSortable.value.destroy()
|
||||
groupSortable.value = null
|
||||
}
|
||||
if (poolSortable.value) {
|
||||
poolSortable.value.destroy()
|
||||
poolSortable.value = null
|
||||
}
|
||||
dropSortables.value.forEach((instance) => instance.destroy())
|
||||
dropSortables.value = []
|
||||
}
|
||||
|
||||
async function syncSortables() {
|
||||
await nextTick()
|
||||
if (canEdit.value) {
|
||||
await initSortables()
|
||||
}
|
||||
}
|
||||
|
||||
function createGroupName() {
|
||||
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
||||
const index = groups.value.length
|
||||
if (index < alphabet.length) return alphabet[index]
|
||||
return `Tier ${index + 1}`
|
||||
}
|
||||
|
||||
async function addGroup() {
|
||||
groups.value = [
|
||||
...groups.value,
|
||||
{
|
||||
id: `g-${Date.now()}-${Math.random().toString(16).slice(2, 8)}`,
|
||||
name: createGroupName(),
|
||||
itemIds: [],
|
||||
},
|
||||
]
|
||||
await syncSortables()
|
||||
}
|
||||
|
||||
async function removeGroup(groupId) {
|
||||
if (groups.value.length <= 1) return
|
||||
const target = groups.value.find((group) => group.id === groupId)
|
||||
if (!target) return
|
||||
pool.value = [...target.itemIds, ...pool.value]
|
||||
groups.value = groups.value.filter((group) => group.id !== groupId)
|
||||
delete groupDropEls.value[groupId]
|
||||
await syncSortables()
|
||||
}
|
||||
|
||||
function addCustomImage(file) {
|
||||
@@ -278,6 +335,10 @@ onMounted(() => {
|
||||
}
|
||||
})()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
destroySortables()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -314,11 +375,15 @@ onMounted(() => {
|
||||
|
||||
<section class="layout">
|
||||
<div ref="boardEl" class="board">
|
||||
<div v-if="canEdit" class="boardTools">
|
||||
<button class="btn btn--ghost" @click="addGroup">티어 추가</button>
|
||||
</div>
|
||||
<div ref="groupListEl" class="rows">
|
||||
<div v-for="g in groups" :key="g.id" class="row">
|
||||
<div class="row__label">
|
||||
<span class="grab" title="드래그로 순서 변경" data-group-handle>↕</span>
|
||||
<input v-model="g.name" class="groupName" :readonly="!canEdit" />
|
||||
<button v-if="canEdit" class="rowRemoveBtn" :disabled="groups.length <= 1" @click="removeGroup(g.id)">삭제</button>
|
||||
</div>
|
||||
<div
|
||||
class="row__drop"
|
||||
@@ -471,6 +536,11 @@ onMounted(() => {
|
||||
border-radius: 16px;
|
||||
padding: 12px;
|
||||
}
|
||||
.boardTools {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.rows {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
@@ -517,6 +587,20 @@ onMounted(() => {
|
||||
outline: none;
|
||||
min-width: 0;
|
||||
}
|
||||
.rowRemoveBtn {
|
||||
padding: 6px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(239, 68, 68, 0.28);
|
||||
background: rgba(239, 68, 68, 0.12);
|
||||
color: rgba(255, 255, 255, 0.92);
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.rowRemoveBtn:disabled {
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.row__drop {
|
||||
border-radius: 14px;
|
||||
background: rgba(0, 0, 0, 0.18);
|
||||
|
||||
Reference in New Issue
Block a user