2026-04-14 15:54:53 +02:00

333 lines
10 KiB
Vue

<template>
<Transition name="overlay">
<div v-if="visible" class="sell-overlay">
<!-- Header -->
<div class="sell-header">
<div class="header-left">
<div class="header-icon">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>
</svg>
</div>
<h2>{{ dealerLabel }}</h2>
</div>
<button class="close-btn" @click="close">
<svg width="14" height="14" viewBox="0 0 14 14" fill="none"><path d="M1 1l12 12M13 1L1 13" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>
</button>
</div>
<!-- Body -->
<div class="sell-body">
<!-- Sell Progress Bar (active sell timer) -->
<div v-if="sellProgress && !sellProgress.isComplete" class="sell-progress-bar">
<div class="progress-bar-left">
<img :src="getItemImage(sellProgress.itemName)" @error="onImgError" class="progress-bar-img" />
</div>
<div class="progress-bar-center">
<div class="progress-bar-label">
<span class="progress-bar-name">{{ sellProgress.itemLabel }} verkaufen</span>
<span class="progress-bar-money">${{ sellProgress.totalMoney }}</span>
</div>
<div class="progress-bar-track">
<div class="progress-bar-fill" :style="{ width: progressPercent + '%' }"></div>
</div>
</div>
<div class="progress-bar-time">
{{ formatTime(remainingTime) }}
</div>
</div>
<!-- Collect Bar (money ready to pick up) -->
<div v-if="pendingCollect" class="collect-bar">
<div class="collect-bar-top">
<img :src="getItemImage(pendingCollect.itemName)" @error="onImgError" class="collect-bar-img" />
<div class="collect-bar-info">
<span class="collect-bar-name">{{ pendingCollect.itemLabel }} - Geld bereit!</span>
<span class="collect-bar-amount">{{ pendingCollect.amount }}x verkauft</span>
</div>
</div>
<button class="collect-btn" @click="collectMoney">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/></svg>
${{ pendingCollect.totalMoney }} abholen
</button>
</div>
<Transition name="fade" mode="out-in">
<!-- Landing Tile -->
<div v-if="showLanding" key="landing" class="sell-landing">
<div
class="dealer-tile"
:style="tileStyle"
@click="enterItems"
>
<div class="dealer-tile-bg" :style="tileBgStyle"></div>
<div v-if="!dealerImage" class="dealer-tile-gradient"></div>
<div v-if="!dealerImage" class="dealer-tile-fallback-icon" :style="{ color: dealerColor || 'var(--accent)' }">
<svg width="64" height="64" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>
</svg>
</div>
<div class="dealer-tile-content">
<div class="dealer-tile-icon" :style="{ color: dealerColor || 'var(--accent)' }">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 1v22M17 5H9.5a3.5 3.5 0 000 7h5a3.5 3.5 0 010 7H6"/>
</svg>
</div>
<span class="dealer-tile-label">{{ dealerLabel }}</span>
<span class="dealer-tile-count">{{ prices.length }} {{ prices.length === 1 ? 'Item' : 'Items' }}</span>
</div>
</div>
</div>
<!-- Sell Menu -->
<div v-else key="menu">
<SellMenu
:prices="prices"
:playerItems="playerItems"
:selectedItem="selectedItem"
:isSelling="!!sellProgress && !sellProgress.isComplete"
@select="selectedItem = $event"
@sell="onSell"
@backToLanding="backToLanding"
/>
</div>
</Transition>
</div>
</div>
</Transition>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
import SellMenu from './components/SellMenu.vue'
const visible = ref(false)
const dealerLabel = ref('')
const dealerId = ref('')
const dealerImage = ref('')
const dealerColor = ref('')
const showLanding = ref(true)
const prices = ref([])
const playerItems = ref([])
const selectedItem = ref(null)
const sellProgress = ref(null)
const pendingCollect = ref(null)
let progressTimer = null
const tileStyle = computed(() => {
const vars = {}
if (dealerColor.value) {
vars['--tile-color'] = dealerColor.value
vars['--tile-glow'] = dealerColor.value + '40'
}
return vars
})
const tileBgStyle = computed(() => {
if (dealerImage.value) {
return { backgroundImage: `url(${dealerImage.value})` }
}
return {}
})
function enterItems() {
showLanding.value = false
}
function backToLanding() {
showLanding.value = true
selectedItem.value = null
}
const progressPercent = computed(() => {
if (!sellProgress.value || !sellProgress.value.totalDuration) return 0
const elapsed = sellProgress.value.elapsed || 0
return Math.min((elapsed / sellProgress.value.totalDuration) * 100, 100)
})
const remainingTime = computed(() => {
if (!sellProgress.value) return 0
return Math.max(0, sellProgress.value.totalDuration - (sellProgress.value.elapsed || 0))
})
function formatTime(seconds) {
if (!seconds || seconds <= 0) return '0s'
const m = Math.floor(seconds / 60)
const s = Math.ceil(seconds % 60)
if (m > 0) return m + 'm ' + s + 's'
return s + 's'
}
function startProgressTimer() {
if (progressTimer) clearInterval(progressTimer)
progressTimer = setInterval(() => {
if (!sellProgress.value) { clearInterval(progressTimer); return }
const sp = sellProgress.value
const newElapsed = (sp.elapsed || 0) + 0.5
const isComplete = newElapsed >= sp.totalDuration
sellProgress.value = { ...sp, elapsed: newElapsed, isComplete }
if (isComplete) {
clearInterval(progressTimer)
progressTimer = null
// Move to pendingCollect
pendingCollect.value = {
itemName: sp.itemName,
itemLabel: sp.itemLabel,
amount: sp.amount,
totalMoney: sp.totalMoney,
}
sellProgress.value = null
}
}, 500)
}
function onSell(data) {
fetch(`https://${GetParentResourceName()}/sellItems`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ itemName: data.itemName, amount: data.amount }),
})
}
function collectMoney() {
fetch(`https://${GetParentResourceName()}/collectMoney`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
}
function close() {
visible.value = false
selectedItem.value = null
fetch(`https://${GetParentResourceName()}/close`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
}
function GetParentResourceName() {
return window.GetParentResourceName ? window.GetParentResourceName() : 'mercy-sell'
}
function getItemImage(name) {
return `https://cfx-nui-codem-inventory/html/itemimages/${name || 'example'}.png`
}
function onImgError(e) {
if (!e.target.src.endsWith('example.png')) e.target.src = './items/example.png'
}
function handleMessage(event) {
const data = event.data
switch (data.type) {
case 'open':
visible.value = true
dealerLabel.value = data.dealerLabel || 'Dealer'
dealerId.value = data.dealerId || ''
dealerImage.value = data.dealerImage || ''
dealerColor.value = data.dealerColor || ''
showLanding.value = true
prices.value = data.prices || []
playerItems.value = data.playerItems || []
selectedItem.value = null
break
case 'close':
visible.value = false
selectedItem.value = null
break
case 'sellStarted':
sellProgress.value = {
itemName: data.itemName,
itemLabel: data.itemLabel,
amount: data.amount,
totalMoney: data.totalMoney,
totalDuration: data.totalDuration,
elapsed: 0,
isComplete: false,
}
pendingCollect.value = null
selectedItem.value = null
showLanding.value = false
startProgressTimer()
break
case 'sellStatus':
if (data.isComplete) {
// Timer done, show collect button
sellProgress.value = null
pendingCollect.value = {
itemName: data.itemName,
itemLabel: data.itemLabel,
amount: data.amount,
totalMoney: data.totalMoney,
}
} else {
sellProgress.value = {
itemName: data.itemName,
itemLabel: data.itemLabel,
amount: data.amount,
totalMoney: data.totalMoney,
totalDuration: data.totalDuration,
elapsed: data.elapsed,
isComplete: false,
}
pendingCollect.value = null
showLanding.value = false
startProgressTimer()
}
break
case 'sellComplete':
// Server confirmed collection
pendingCollect.value = null
sellProgress.value = null
if (progressTimer) { clearInterval(progressTimer); progressTimer = null }
break
case 'sellFailed':
break
case 'updateInventory':
playerItems.value = data.playerItems || []
break
case 'updatePrices':
prices.value = data.prices || []
break
}
}
function handleKeydown(e) {
if (e.key === 'Escape' && visible.value) {
if (selectedItem.value) {
// Detail → List
selectedItem.value = null
e.stopPropagation()
} else if (!showLanding.value) {
// List → Landing
backToLanding()
e.stopPropagation()
} else {
// Landing → Close
close()
}
}
}
onMounted(() => {
window.addEventListener('message', handleMessage)
window.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
window.removeEventListener('message', handleMessage)
window.removeEventListener('keydown', handleKeydown)
if (progressTimer) clearInterval(progressTimer)
})
</script>