Added Calculator
This commit is contained in:
235
frontend/components/Calculator.vue
Normal file
235
frontend/components/Calculator.vue
Normal file
@@ -0,0 +1,235 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user