289 lines
9.8 KiB
Vue
289 lines
9.8 KiB
Vue
<template>
|
|
<div
|
|
class="z-[1] select-none touch-none"
|
|
:class="selected ? 'outline outline-2 outline-accent outline-offset-2' : ''"
|
|
:style="computedStyle"
|
|
@mousedown.stop="$emit('drag-start', { id: widget.id, event: $event })"
|
|
@touchstart.stop="$emit('drag-start', { id: widget.id, event: $event })"
|
|
@click.stop="$emit('select')"
|
|
>
|
|
<!-- Bubble Style -->
|
|
<template v-if="widget.cardStyle !== 1">
|
|
<!-- Central Bubble -->
|
|
<div class="absolute rounded-full flex flex-col items-center justify-center" :style="roomCardBubbleStyle">
|
|
<span v-if="widget.iconCodepoint" class="material-symbols-outlined" :style="roomCardIconStyle">
|
|
{{ iconChar }}
|
|
</span>
|
|
<span v-if="roomCardParts.name" class="leading-tight font-semibold" :style="roomCardNameStyle">{{ roomCardParts.name }}</span>
|
|
<span v-if="roomCardParts.format" class="leading-tight opacity-70" :style="roomCardTempStyle">{{ roomCardParts.format }}</span>
|
|
</div>
|
|
<!-- Sub-Buttons (circular orbit) -->
|
|
<div
|
|
v-for="(sb, idx) in roomCardSubButtons"
|
|
:key="idx"
|
|
class="absolute rounded-full flex items-center justify-center shadow-md"
|
|
:style="getSubButtonStyle(sb, idx)"
|
|
>
|
|
<span v-if="sb.icon" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
|
|
{{ String.fromCodePoint(sb.icon) }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Tile Style -->
|
|
<template v-else>
|
|
<div class="absolute inset-0 overflow-hidden" :style="roomCardTileContainerStyle">
|
|
<!-- Room name (top-left) -->
|
|
<div class="absolute" :style="{ left: '16px', top: '12px', color: widget.textColor, fontSize: fontSizes[widget.fontSize || 2] + 'px', fontWeight: 600 }">
|
|
{{ roomCardParts.name }}
|
|
</div>
|
|
<!-- Text Lines -->
|
|
<div class="absolute flex flex-col gap-1" :style="textLinesContainerStyle">
|
|
<span v-for="(line, idx) in textLines" :key="idx" class="flex items-center gap-1" :style="getTextLineStyle(line)">
|
|
<span v-if="line.icon" class="material-symbols-outlined" :style="{ fontSize: getTextLineIconSize(line) + 'px' }">{{ String.fromCodePoint(line.icon) }}</span>
|
|
<span>{{ line.text || '--' }}</span>
|
|
</span>
|
|
</div>
|
|
<!-- Large decorative icon (configurable position) -->
|
|
<span v-if="widget.iconCodepoint" class="material-symbols-outlined absolute opacity-20" :style="decorIconStyle">
|
|
{{ iconChar }}
|
|
</span>
|
|
</div>
|
|
<!-- Sub-Buttons (right side, vertical) -->
|
|
<div
|
|
v-for="(sb, idx) in roomCardSubButtons"
|
|
:key="idx"
|
|
class="absolute rounded-full flex items-center justify-center shadow-md"
|
|
:style="getSubButtonStyleTile(sb, idx)"
|
|
>
|
|
<span v-if="sb.icon" class="material-symbols-outlined" :style="getSubButtonIconStyle(sb)">
|
|
{{ String.fromCodePoint(sb.icon) }}
|
|
</span>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- Resize Handle -->
|
|
<div
|
|
v-if="selected"
|
|
class="absolute -right-1.5 -bottom-1.5 w-3.5 h-3.5 rounded-[4px] bg-accent border-[2px] border-[#1b1308] shadow-[0_4px_12px_rgba(0,0,0,0.35)] cursor-se-resize z-10"
|
|
data-resize-handle
|
|
@mousedown.stop="$emit('resize-start', { id: widget.id, event: $event })"
|
|
@touchstart.stop="$emit('resize-start', { id: widget.id, event: $event })"
|
|
>
|
|
<span class="absolute right-[2px] bottom-[2px] w-[6px] h-[6px] border-r-2 border-b-2 border-[#1b1308a6] rounded-[2px]"></span>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed } from 'vue';
|
|
import { fontSizes, iconFontSizes } from '../../../constants';
|
|
import { getBaseStyle, getBorderStyle, clamp, hexToRgba } from '../shared/utils';
|
|
|
|
const props = defineProps({
|
|
widget: { type: Object, required: true },
|
|
scale: { type: Number, default: 1 },
|
|
selected: { type: Boolean, default: false }
|
|
});
|
|
|
|
defineEmits(['select', 'drag-start', 'resize-start']);
|
|
|
|
const iconChar = computed(() => {
|
|
const cp = props.widget.iconCodepoint || 0xe88a;
|
|
return String.fromCodePoint(cp);
|
|
});
|
|
|
|
const roomCardParts = computed(() => {
|
|
if (!props.widget.text) return { name: '', format: '' };
|
|
const parts = props.widget.text.split('\n');
|
|
return {
|
|
name: parts[0] || '',
|
|
format: props.widget.cardStyle === 1 ? '' : (parts[1] || '')
|
|
};
|
|
});
|
|
|
|
const roomCardSubButtons = computed(() => props.widget.subButtons || []);
|
|
const textLines = computed(() => props.widget.textLines || []);
|
|
|
|
const textLinesContainerStyle = computed(() => {
|
|
const s = props.scale;
|
|
// Calculate top based on room name position + font size + padding
|
|
const roomNameTop = 12;
|
|
const roomNameFontSize = fontSizes[props.widget.fontSize || 2] || 22;
|
|
const padding = 10;
|
|
const top = (roomNameTop + roomNameFontSize + padding) * s;
|
|
return {
|
|
left: (16 * s) + 'px',
|
|
top: top + 'px',
|
|
color: props.widget.textColor
|
|
};
|
|
});
|
|
|
|
function getTextLineStyle(line) {
|
|
const s = props.scale;
|
|
const sizeIdx = line.fontSize ?? 1;
|
|
return {
|
|
fontSize: (fontSizes[sizeIdx] * s) + 'px'
|
|
};
|
|
}
|
|
|
|
function getTextLineIconSize(line) {
|
|
const s = props.scale;
|
|
const sizeIdx = line.fontSize ?? 1;
|
|
return fontSizes[sizeIdx] * s;
|
|
}
|
|
|
|
const roomCardTileContainerStyle = computed(() => {
|
|
const alpha = (props.widget.bgOpacity ?? 255) / 255;
|
|
return {
|
|
backgroundColor: hexToRgba(props.widget.bgColor || '#333333', alpha),
|
|
borderRadius: (props.widget.radius || 16) + 'px',
|
|
};
|
|
});
|
|
|
|
const decorIconStyle = computed(() => {
|
|
const s = props.scale;
|
|
const w = props.widget;
|
|
const x = (w.iconPositionX !== undefined && w.iconPositionX !== 0) ? w.iconPositionX * s : -20 * s;
|
|
const y = (w.iconPositionY !== undefined && w.iconPositionY !== 0) ? w.iconPositionY * s : (w.h - 120) * s;
|
|
const sizeIdx = w.iconSize >= 6 ? w.iconSize : 8;
|
|
const fontSize = iconFontSizes[sizeIdx] || 96;
|
|
|
|
return {
|
|
left: x + 'px',
|
|
top: y + 'px',
|
|
fontSize: (fontSize * s) + 'px',
|
|
color: w.textColor,
|
|
};
|
|
});
|
|
|
|
const getSubButtonStyleTile = (sb, idx) => {
|
|
const s = props.scale;
|
|
const btnSize = (props.widget.subButtonSize || 40) * s;
|
|
const gap = 10 * s;
|
|
const padding = 12 * s;
|
|
|
|
return {
|
|
width: btnSize + 'px',
|
|
height: btnSize + 'px',
|
|
right: padding + 'px',
|
|
top: (padding + idx * (btnSize + gap)) + 'px',
|
|
backgroundColor: sb.colorOff || '#666666',
|
|
};
|
|
};
|
|
|
|
const roomCardBubbleStyle = computed(() => {
|
|
const s = props.scale;
|
|
const w = props.widget.w;
|
|
const h = props.widget.h;
|
|
const subBtnSize = (props.widget.subButtonSize || 40) * s;
|
|
const padding = subBtnSize * 0.6;
|
|
const bubbleSize = Math.min(w, h) * s - padding * 2;
|
|
const left = (w * s - bubbleSize) / 2;
|
|
const top = (h * s - bubbleSize) / 2;
|
|
const alpha = clamp((props.widget.bgOpacity ?? 255) / 255, 0, 1);
|
|
|
|
return {
|
|
width: `${bubbleSize}px`,
|
|
height: `${bubbleSize}px`,
|
|
left: `${left}px`,
|
|
top: `${top}px`,
|
|
backgroundColor: hexToRgba(props.widget.bgColor, alpha),
|
|
color: props.widget.textColor,
|
|
border: ((props.widget.borderWidth || 0) > 0 && (props.widget.borderOpacity ?? 0) > 0)
|
|
? `${(props.widget.borderWidth || 0) * s}px solid ${hexToRgba(props.widget.borderColor || '#ffffff', clamp((props.widget.borderOpacity ?? 0) / 255, 0, 1))}`
|
|
: 'none',
|
|
boxShadow: props.widget.shadow?.enabled
|
|
? `${(props.widget.shadow.x || 0) * s}px ${(props.widget.shadow.y || 0) * s}px ${(props.widget.shadow.blur || 0) * s}px ${hexToRgba(props.widget.shadow.color || '#000000', 0.3)}`
|
|
: '0 4px 12px rgba(0,0,0,0.15)'
|
|
};
|
|
});
|
|
|
|
const roomCardIconStyle = computed(() => {
|
|
const s = props.scale;
|
|
const sizeIdx = props.widget.iconSize ?? 3;
|
|
const size = fontSizes[sizeIdx] || 28;
|
|
return {
|
|
fontSize: `${size * s}px`,
|
|
color: props.widget.textColor
|
|
};
|
|
});
|
|
|
|
const roomCardNameStyle = computed(() => {
|
|
const s = props.scale;
|
|
const sizeIdx = props.widget.fontSize ?? 2;
|
|
const size = fontSizes[sizeIdx] || 22;
|
|
return {
|
|
fontSize: `${size * s * 0.7}px`,
|
|
color: props.widget.textColor
|
|
};
|
|
});
|
|
|
|
const roomCardTempStyle = computed(() => {
|
|
const s = props.scale;
|
|
const sizeIdx = props.widget.fontSize ?? 2;
|
|
const size = fontSizes[sizeIdx] || 22;
|
|
return {
|
|
fontSize: `${size * s * 0.55}px`,
|
|
color: props.widget.textColor
|
|
};
|
|
});
|
|
|
|
function getSubButtonStyle(sb, idx) {
|
|
const s = props.scale;
|
|
const w = props.widget.w;
|
|
const h = props.widget.h;
|
|
const subBtnSize = (props.widget.subButtonSize || 40) * s;
|
|
const centerX = w * s / 2;
|
|
const centerY = h * s / 2;
|
|
const orbitRadius = (props.widget.subButtonDistance || 80) * s;
|
|
const pos = sb.pos ?? idx;
|
|
const angle = (pos * (Math.PI / 4)) - (Math.PI / 2);
|
|
const x = centerX + orbitRadius * Math.cos(angle) - subBtnSize / 2;
|
|
const y = centerY + orbitRadius * Math.sin(angle) - subBtnSize / 2;
|
|
const bgColor = sb.colorOff || '#666666';
|
|
|
|
return {
|
|
width: `${subBtnSize}px`,
|
|
height: `${subBtnSize}px`,
|
|
left: `${x}px`,
|
|
top: `${y}px`,
|
|
backgroundColor: bgColor,
|
|
border: `2px solid ${hexToRgba('#ffffff', 0.3)}`
|
|
};
|
|
}
|
|
|
|
function getSubButtonIconStyle(sb) {
|
|
const s = props.scale;
|
|
const subBtnSize = (props.widget.subButtonSize || 40) * s;
|
|
const iconSize = subBtnSize * 0.5;
|
|
return {
|
|
fontSize: `${iconSize}px`,
|
|
color: '#ffffff'
|
|
};
|
|
}
|
|
|
|
const computedStyle = computed(() => {
|
|
const w = props.widget;
|
|
const s = props.scale;
|
|
|
|
const style = getBaseStyle(w, s);
|
|
|
|
if (w.cardStyle === 1) {
|
|
const alpha = clamp((w.bgOpacity ?? 255) / 255, 0, 1);
|
|
style.background = hexToRgba(w.bgColor || '#333333', alpha);
|
|
style.borderRadius = `${(w.radius || 16) * s}px`;
|
|
style.overflow = 'hidden';
|
|
if ((w.borderWidth || 0) > 0 && (w.borderOpacity ?? 0) > 0) {
|
|
Object.assign(style, getBorderStyle(w, s));
|
|
} else if (w.shadow && w.shadow.enabled) {
|
|
style.border = `3px solid ${w.shadow.color || '#ff6b6b'}`;
|
|
}
|
|
} else {
|
|
style.overflow = 'visible';
|
|
}
|
|
|
|
return style;
|
|
});
|
|
</script>
|