235 lines
8.2 KiB
Vue
235 lines
8.2 KiB
Vue
<template>
|
||
<div
|
||
ref="el"
|
||
:style="style"
|
||
class="fixed z-[999] w-72 bg-white dark:bg-gray-900 shadow-2xl rounded-xl border border-gray-200 dark:border-gray-800 p-4 select-none touch-none"
|
||
>
|
||
<div class="flex items-center justify-between mb-4 cursor-move border-b pb-2 dark:border-gray-800">
|
||
<div class="flex items-center gap-2 text-gray-500">
|
||
<UIcon name="i-heroicons-calculator" />
|
||
<span class="text-xs font-bold uppercase tracking-wider">Kalkulator</span>
|
||
</div>
|
||
<div class="flex items-center gap-1">
|
||
<UTooltip text="Verlauf">
|
||
<UButton
|
||
color="gray"
|
||
variant="ghost"
|
||
:icon="showHistory ? 'i-heroicons-clock-solid' : 'i-heroicons-clock'"
|
||
size="xs"
|
||
@click="showHistory = !showHistory"
|
||
/>
|
||
</UTooltip>
|
||
<UTooltip text="Schließen (Esc)">
|
||
<UButton
|
||
color="gray"
|
||
variant="ghost"
|
||
icon="i-heroicons-x-mark"
|
||
size="xs"
|
||
@click="store.isOpen = false"
|
||
/>
|
||
</UTooltip>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="!showHistory">
|
||
<div
|
||
class="bg-gray-100 dark:bg-gray-800 p-3 rounded-lg mb-4 text-right border border-gray-200 dark:border-gray-700 cursor-pointer group relative"
|
||
@click="copyDisplay"
|
||
>
|
||
<div class="text-[10px] text-gray-500 h-4 font-mono uppercase tracking-tighter">
|
||
Speicher: {{ Number(store.memory).toFixed(2).replace('.', ',') }} €
|
||
</div>
|
||
<div class="text-2xl font-mono truncate tracking-tighter">{{ store.display }}</div>
|
||
|
||
<div class="absolute inset-0 flex items-center justify-center bg-primary-500/10 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg">
|
||
<span class="text-[10px] font-bold text-primary-600 uppercase">
|
||
{{ copied ? 'Kopiert!' : 'Klicken zum Kopieren' }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-4 gap-2">
|
||
<UTooltip text="Brutto (+19%)"><UButton color="green" variant="soft" block size="xs" @click="applyTax(19)">+19%</UButton></UTooltip>
|
||
<UTooltip text="Brutto (+7%)"><UButton color="green" variant="soft" block size="xs" @click="applyTax(7)">+7%</UButton></UTooltip>
|
||
<UTooltip text="Netto (-19%)"><UButton color="rose" variant="soft" block size="xs" @click="removeTax(19)">-19%</UButton></UTooltip>
|
||
<UTooltip text="Netto (-7%)"><UButton color="rose" variant="soft" block size="xs" @click="removeTax(7)">-7%</UButton></UTooltip>
|
||
|
||
<UTooltip text="Löschen"><UButton color="gray" variant="ghost" block @click="clear">C</UButton></UTooltip>
|
||
<UTooltip text="Speicher +"><UButton color="gray" variant="ghost" block @click="addToSum">M+</UButton></UTooltip>
|
||
<UTooltip text="Speicher Reset"><UButton color="gray" variant="ghost" block @click="store.memory = 0">MC</UButton></UTooltip>
|
||
<UButton color="primary" variant="soft" @click="setOperator('/')">/</UButton>
|
||
|
||
<UButton v-for="n in [7, 8, 9]" :key="n" color="white" @click="appendNumber(n)">{{ n }}</UButton>
|
||
<UButton color="primary" variant="soft" @click="setOperator('*')">×</UButton>
|
||
|
||
<UButton v-for="n in [4, 5, 6]" :key="n" color="white" @click="appendNumber(n)">{{ n }}</UButton>
|
||
<UButton color="primary" variant="soft" @click="setOperator('-')">-</UButton>
|
||
|
||
<UButton v-for="n in [1, 2, 3]" :key="n" color="white" @click="appendNumber(n)">{{ n }}</UButton>
|
||
<UButton color="primary" variant="soft" @click="setOperator('+')">+</UButton>
|
||
|
||
<UButton color="white" class="col-span-2" @click="appendNumber(0)">0</UButton>
|
||
<UButton color="white" @click="addComma">,</UButton>
|
||
<UButton color="primary" block @click="calculate">=</UButton>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-else class="h-[270px] flex flex-col animate-in fade-in duration-200">
|
||
<div class="flex-1 overflow-y-auto space-y-2 pr-1 custom-scrollbar">
|
||
<div v-if="store.history.length === 0" class="text-center text-gray-400 text-xs mt-10 italic">
|
||
Keine Berechnungen im Verlauf
|
||
</div>
|
||
<div
|
||
v-for="(item, i) in store.history" :key="i"
|
||
class="p-2 bg-gray-50 dark:bg-gray-800 rounded text-right border-l-2 border-primary-500 cursor-pointer hover:bg-primary-50 dark:hover:bg-primary-900/20 transition-colors"
|
||
@click="useHistoryItem(item.result)"
|
||
>
|
||
<div class="text-[10px] text-gray-400">{{ item.expression }} =</div>
|
||
<div class="text-sm font-bold">{{ item.result }}</div>
|
||
</div>
|
||
</div>
|
||
<UButton
|
||
color="gray"
|
||
variant="ghost"
|
||
size="xs"
|
||
block
|
||
class="mt-2"
|
||
icon="i-heroicons-trash"
|
||
@click="store.history = []"
|
||
>
|
||
Verlauf leeren
|
||
</UButton>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { useDraggable, useClipboard } from '@vueuse/core'
|
||
import { useCalculatorStore } from '~/stores/calculator'
|
||
|
||
const store = useCalculatorStore()
|
||
const { copy, copied } = useClipboard()
|
||
|
||
const el = ref(null)
|
||
const { style } = useDraggable(el, {
|
||
initialValue: { x: window.innerWidth - 350, y: 150 },
|
||
})
|
||
|
||
const shouldResetDisplay = ref(false)
|
||
const showHistory = ref(false)
|
||
const previousValue = ref(null)
|
||
const lastOperator = ref(null)
|
||
|
||
// --- Logik ---
|
||
const appendNumber = (num) => {
|
||
if (store.display === '0' || shouldResetDisplay.value) {
|
||
store.display = String(num)
|
||
shouldResetDisplay.value = false
|
||
} else {
|
||
store.display += String(num)
|
||
}
|
||
}
|
||
|
||
const addComma = () => {
|
||
if (!store.display.includes(',')) {
|
||
store.display += ','
|
||
}
|
||
}
|
||
|
||
const setOperator = (op) => {
|
||
previousValue.value = parseFloat(store.display.replace(',', '.'))
|
||
lastOperator.value = op
|
||
shouldResetDisplay.value = true
|
||
}
|
||
|
||
const calculate = () => {
|
||
if (lastOperator.value === null) return
|
||
const currentVal = parseFloat(store.display.replace(',', '.'))
|
||
const prevVal = previousValue.value
|
||
let result = 0
|
||
|
||
switch (lastOperator.value) {
|
||
case '+': result = prevVal + currentVal; break
|
||
case '-': result = prevVal - currentVal; break
|
||
case '*': result = prevVal * currentVal; break
|
||
case '/': result = currentVal !== 0 ? prevVal / currentVal : 0; break
|
||
}
|
||
|
||
const expression = `${prevVal} ${lastOperator.value} ${currentVal}`
|
||
const resultString = String(Number(result.toFixed(4))).replace('.', ',')
|
||
|
||
store.addHistory(expression, resultString)
|
||
store.display = resultString
|
||
lastOperator.value = null
|
||
shouldResetDisplay.value = true
|
||
}
|
||
|
||
const clear = () => {
|
||
store.display = '0'
|
||
previousValue.value = null
|
||
lastOperator.value = null
|
||
}
|
||
|
||
const applyTax = (percent) => {
|
||
const current = parseFloat(store.display.replace(',', '.'))
|
||
store.display = (current * (1 + percent / 100)).toFixed(2).replace('.', ',')
|
||
}
|
||
|
||
const removeTax = (percent) => {
|
||
const current = parseFloat(store.display.replace(',', '.'))
|
||
store.display = (current / (1 + percent / 100)).toFixed(2).replace('.', ',')
|
||
}
|
||
|
||
const addToSum = () => {
|
||
store.memory += parseFloat(store.display.replace(',', '.'))
|
||
shouldResetDisplay.value = true
|
||
}
|
||
|
||
const copyDisplay = () => {
|
||
copy(store.display)
|
||
}
|
||
|
||
const useHistoryItem = (val) => {
|
||
store.display = val
|
||
showHistory.value = false
|
||
}
|
||
|
||
// --- Shortcuts ---
|
||
defineShortcuts({
|
||
'0': () => appendNumber(0),
|
||
'1': () => appendNumber(1),
|
||
'2': () => appendNumber(2),
|
||
'3': () => appendNumber(3),
|
||
'4': () => appendNumber(4),
|
||
'5': () => appendNumber(5),
|
||
'6': () => appendNumber(6),
|
||
'7': () => appendNumber(7),
|
||
'8': () => appendNumber(8),
|
||
'9': () => appendNumber(9),
|
||
'comma': addComma,
|
||
'plus': () => setOperator('+'),
|
||
'minus': () => setOperator('-'),
|
||
'enter': { usingInput: true, handler: calculate },
|
||
'backspace': () => {
|
||
store.display = store.display.length > 1 ? store.display.slice(0, -1) : '0'
|
||
},
|
||
// Escape schließt nun das Fenster via Store
|
||
'escape': {
|
||
usingInput: true,
|
||
whenever: [computed(() => store.isOpen)],
|
||
handler: () => { store.isOpen = false }
|
||
}
|
||
})
|
||
</script>
|
||
|
||
<style scoped>
|
||
.custom-scrollbar::-webkit-scrollbar {
|
||
width: 4px;
|
||
}
|
||
.custom-scrollbar::-webkit-scrollbar-track {
|
||
@apply bg-transparent;
|
||
}
|
||
.custom-scrollbar::-webkit-scrollbar-thumb {
|
||
@apply bg-gray-200 dark:bg-gray-700 rounded-full;
|
||
}
|
||
</style> |