|
|
@@ -0,0 +1,397 @@
|
|
|
+<template>
|
|
|
+ <div :style="styleMap?.mainStyle" class="relative w-full h-full box-border overflow-hidden">
|
|
|
+ <ImageBg
|
|
|
+ v-if="styleMap?.mainStyle?.imageSrc"
|
|
|
+ :src="styleMap?.mainStyle?.imageSrc"
|
|
|
+ :imageStyle="styleMap?.mainStyle?.imageStyle"
|
|
|
+ />
|
|
|
+
|
|
|
+ <div class="absolute left-0 top-0 overflow-hidden" :style="canvasStyle">
|
|
|
+ <div class="relative overflow-hidden box-border" :style="scaledInnerStyle">
|
|
|
+ <div
|
|
|
+ v-if="showHeader"
|
|
|
+ class="relative flex items-center justify-between"
|
|
|
+ :style="headerStyle"
|
|
|
+ >
|
|
|
+ <ImageBg
|
|
|
+ v-if="styleMap?.headerStyle?.imageSrc"
|
|
|
+ :src="styleMap?.headerStyle?.imageSrc"
|
|
|
+ :imageStyle="styleMap?.headerStyle?.imageStyle"
|
|
|
+ />
|
|
|
+
|
|
|
+ <template v-if="props.titleMode === 'arrow-buttons'">
|
|
|
+ <button class="calendar-nav-btn" :style="navButtonStyle">
|
|
|
+ <i
|
|
|
+ class="lvgl-icon not-italic"
|
|
|
+ v-html="getSymbol('LV_SYMBOL_LEFT')"
|
|
|
+ :style="navIconStyle"
|
|
|
+ ></i>
|
|
|
+ </button>
|
|
|
+ <div class="relative z-1 flex-1 text-center font-600" :style="headerTextStyle">
|
|
|
+ {{ titleText }}
|
|
|
+ </div>
|
|
|
+ <button class="calendar-nav-btn" :style="navButtonStyle">
|
|
|
+ <i
|
|
|
+ class="lvgl-icon not-italic"
|
|
|
+ v-html="getSymbol('LV_SYMBOL_RIGHT')"
|
|
|
+ :style="navIconStyle"
|
|
|
+ ></i>
|
|
|
+ </button>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <template v-else>
|
|
|
+ <div class="calendar-select-row relative z-1">
|
|
|
+ <div class="calendar-select-box" :style="dropdownStyle">
|
|
|
+ <span :style="headerTextStyle">{{ displayYear }}</span>
|
|
|
+ <i
|
|
|
+ class="lvgl-icon not-italic"
|
|
|
+ v-html="getSymbol('LV_SYMBOL_DOWN')"
|
|
|
+ :style="dropdownArrowStyle"
|
|
|
+ ></i>
|
|
|
+ </div>
|
|
|
+ <div class="calendar-select-box" :style="dropdownStyle">
|
|
|
+ <span :style="headerTextStyle">{{ displayMonth }}</span>
|
|
|
+ <i
|
|
|
+ class="lvgl-icon not-italic"
|
|
|
+ v-html="getSymbol('LV_SYMBOL_DOWN')"
|
|
|
+ :style="dropdownArrowStyle"
|
|
|
+ ></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="calendar-body" :style="bodyStyle">
|
|
|
+ <div class="calendar-weekdays">
|
|
|
+ <div
|
|
|
+ v-for="label in weekdayLabels"
|
|
|
+ :key="label"
|
|
|
+ class="calendar-weekday"
|
|
|
+ :style="weekdayStyle"
|
|
|
+ >
|
|
|
+ {{ label }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="calendar-grid">
|
|
|
+ <div
|
|
|
+ v-for="cell in calendarCells"
|
|
|
+ :key="cell.key"
|
|
|
+ class="calendar-cell relative box-border"
|
|
|
+ :style="getCellStyle(cell)"
|
|
|
+ >
|
|
|
+ <ImageBg
|
|
|
+ v-if="styleMap?.itemsStyle?.imageSrc"
|
|
|
+ :src="styleMap?.itemsStyle?.imageSrc"
|
|
|
+ :imageStyle="styleMap?.itemsStyle?.imageStyle"
|
|
|
+ />
|
|
|
+ <div class="relative z-1 flex h-full w-full flex-col items-center justify-center">
|
|
|
+ <span :style="dayTextStyle(cell)">
|
|
|
+ {{ cell.day }}
|
|
|
+ </span>
|
|
|
+ <span v-if="props.showLunar" :style="lunarTextStyle(cell)">
|
|
|
+ {{ cell.lunarLabel }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ <div v-if="cell.isCurrentDate" class="calendar-current-ring" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { computed, type CSSProperties } from 'vue'
|
|
|
+import { useWidgetStyle } from '../hooks/useWidgetStyle'
|
|
|
+import ImageBg from '../ImageBg.vue'
|
|
|
+import { getSymbol } from '@/utils'
|
|
|
+import {
|
|
|
+ buildCalendarCells,
|
|
|
+ formatTitle,
|
|
|
+ getWeekdayLabels,
|
|
|
+ resolveCurrentDate,
|
|
|
+ resolveDisplayDate
|
|
|
+} from './utils'
|
|
|
+
|
|
|
+const BASE_WIDTH = 400
|
|
|
+const BASE_HEIGHT = 320
|
|
|
+const HEADER_HEIGHT = 56
|
|
|
+const HORIZONTAL_PADDING = 12
|
|
|
+const BODY_BOTTOM_PADDING = 12
|
|
|
+
|
|
|
+const props = defineProps<{
|
|
|
+ width: number
|
|
|
+ height: number
|
|
|
+ styles: any
|
|
|
+ state?: string
|
|
|
+ part?: string
|
|
|
+ currentDate?: string
|
|
|
+ displayDate?: string
|
|
|
+ showLunar?: boolean
|
|
|
+ titleMode?: 'none' | 'arrow-buttons' | 'drop-down'
|
|
|
+}>()
|
|
|
+
|
|
|
+const styleMap = useWidgetStyle({
|
|
|
+ widget: 'lv_calendar',
|
|
|
+ props
|
|
|
+})
|
|
|
+
|
|
|
+const weekdayLabels = getWeekdayLabels()
|
|
|
+
|
|
|
+const showHeader = computed(() => props.titleMode !== 'none')
|
|
|
+
|
|
|
+const contentPadding = computed(() => {
|
|
|
+ const padding = String(styleMap.value?.mainStyle?.padding || '0px')
|
|
|
+ .split(' ')
|
|
|
+ .map((item) => Number.parseFloat(item) || 0)
|
|
|
+
|
|
|
+ if (padding.length === 4) {
|
|
|
+ return {
|
|
|
+ top: padding[0],
|
|
|
+ right: padding[1],
|
|
|
+ bottom: padding[2],
|
|
|
+ left: padding[3]
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ top: 0,
|
|
|
+ right: 0,
|
|
|
+ bottom: 0,
|
|
|
+ left: 0
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const canvasStyle = computed((): CSSProperties => {
|
|
|
+ const { top, right, bottom, left } = contentPadding.value
|
|
|
+ const availableWidth = Math.max(props.width - left - right, 0)
|
|
|
+ const availableHeight = Math.max(props.height - top - bottom, 0)
|
|
|
+
|
|
|
+ return {
|
|
|
+ left: `${left}px`,
|
|
|
+ top: `${top}px`,
|
|
|
+ width: `${availableWidth}px`,
|
|
|
+ height: `${availableHeight}px`
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const scaledInnerStyle = computed((): CSSProperties => {
|
|
|
+ const width = Number.parseFloat(String(canvasStyle.value.width || 0)) || 0
|
|
|
+ const height = Number.parseFloat(String(canvasStyle.value.height || 0)) || 0
|
|
|
+ const scaleX = width / BASE_WIDTH || 0
|
|
|
+ const scaleY = height / BASE_HEIGHT || 0
|
|
|
+
|
|
|
+ return {
|
|
|
+ width: `${BASE_WIDTH}px`,
|
|
|
+ height: `${BASE_HEIGHT}px`,
|
|
|
+ transform: `scale(${scaleX}, ${scaleY})`,
|
|
|
+ transformOrigin: 'left top'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const titleText = computed(() => formatTitle(props.displayDate, props.currentDate))
|
|
|
+
|
|
|
+const displayValue = computed(() => {
|
|
|
+ const currentDate = resolveCurrentDate(props.currentDate)
|
|
|
+ return resolveDisplayDate(props.displayDate, currentDate)
|
|
|
+})
|
|
|
+
|
|
|
+const displayYear = computed(() => String(displayValue.value.year))
|
|
|
+const displayMonth = computed(() => String(displayValue.value.month).padStart(2, '0'))
|
|
|
+
|
|
|
+const calendarCells = computed(() => buildCalendarCells(props.displayDate, props.currentDate))
|
|
|
+
|
|
|
+const headerStyle = computed((): CSSProperties => {
|
|
|
+ return {
|
|
|
+ ...styleMap.value?.headerStyle,
|
|
|
+ height: `${HEADER_HEIGHT}px`,
|
|
|
+ marginBottom: '8px',
|
|
|
+ padding: '0 12px',
|
|
|
+ boxSizing: 'border-box'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const headerTextStyle = computed((): CSSProperties => {
|
|
|
+ return {
|
|
|
+ color: styleMap.value?.headerStyle?.color,
|
|
|
+ fontSize: styleMap.value?.headerStyle?.fontSize,
|
|
|
+ fontFamily: styleMap.value?.headerStyle?.fontFamily,
|
|
|
+ fontStyle: styleMap.value?.headerStyle?.fontStyle,
|
|
|
+ fontWeight: styleMap.value?.headerStyle?.fontWeight,
|
|
|
+ textDecoration: styleMap.value?.headerStyle?.textDecoration,
|
|
|
+ letterSpacing: styleMap.value?.headerStyle?.letterSpacing
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const navButtonStyle = computed((): CSSProperties => {
|
|
|
+ return {
|
|
|
+ width: '30px',
|
|
|
+ height: '30px',
|
|
|
+ borderRadius: '999px',
|
|
|
+ border: 'none',
|
|
|
+ background: '#2196f3',
|
|
|
+ color: '#ffffff',
|
|
|
+ padding: 0,
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ position: 'relative',
|
|
|
+ zIndex: 1
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const navIconStyle = computed((): CSSProperties => {
|
|
|
+ return {
|
|
|
+ fontSize: '12px',
|
|
|
+ lineHeight: 1
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const dropdownStyle = computed((): CSSProperties => {
|
|
|
+ return {
|
|
|
+ minWidth: '132px',
|
|
|
+ height: '34px',
|
|
|
+ padding: '0 14px',
|
|
|
+ borderRadius: '10px',
|
|
|
+ border: '1px solid #dedede',
|
|
|
+ backgroundColor: '#ffffff',
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'space-between',
|
|
|
+ boxSizing: 'border-box'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const dropdownArrowStyle = computed((): CSSProperties => {
|
|
|
+ return {
|
|
|
+ color: styleMap.value?.headerStyle?.color,
|
|
|
+ fontSize: '12px',
|
|
|
+ lineHeight: 1,
|
|
|
+ marginLeft: '12px'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const bodyStyle = computed((): CSSProperties => {
|
|
|
+ const topPadding = showHeader.value ? 0 : 10
|
|
|
+ return {
|
|
|
+ height: `${BASE_HEIGHT - (showHeader.value ? HEADER_HEIGHT + 8 : 0)}px`,
|
|
|
+ padding: `${topPadding}px ${HORIZONTAL_PADDING}px ${BODY_BOTTOM_PADDING}px`,
|
|
|
+ boxSizing: 'border-box'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const weekdayStyle = computed((): CSSProperties => {
|
|
|
+ return {
|
|
|
+ ...headerTextStyle.value,
|
|
|
+ fontWeight: 400,
|
|
|
+ opacity: 0.6,
|
|
|
+ textAlign: 'center'
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+const itemGap = computed(() => {
|
|
|
+ if (!props.showLunar) return 0
|
|
|
+ const lineHeightValue = String(styleMap.value?.itemsStyle?.lineHeight || '')
|
|
|
+ const parsedLineHeight = Number.parseFloat(lineHeightValue)
|
|
|
+ const fontSize = Number.parseFloat(String(styleMap.value?.itemsStyle?.fontSize || 16)) || 16
|
|
|
+ if (!parsedLineHeight) return 0
|
|
|
+ return parsedLineHeight - fontSize * 1.2
|
|
|
+})
|
|
|
+
|
|
|
+const dayTextStyle = (cell: { inMonth: boolean }): CSSProperties => {
|
|
|
+ return {
|
|
|
+ color: styleMap.value?.itemsStyle?.color,
|
|
|
+ fontSize: styleMap.value?.itemsStyle?.fontSize,
|
|
|
+ fontFamily: styleMap.value?.itemsStyle?.fontFamily,
|
|
|
+ fontStyle: styleMap.value?.itemsStyle?.fontStyle,
|
|
|
+ fontWeight: styleMap.value?.itemsStyle?.fontWeight || 400,
|
|
|
+ textDecoration: styleMap.value?.itemsStyle?.textDecoration,
|
|
|
+ letterSpacing: styleMap.value?.itemsStyle?.letterSpacing,
|
|
|
+ opacity: cell.inMonth ? 1 : 0.45,
|
|
|
+ lineHeight: 1
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const lunarTextStyle = (cell: { inMonth: boolean }): CSSProperties => {
|
|
|
+ return {
|
|
|
+ ...dayTextStyle(cell),
|
|
|
+ fontSize: '0.85em',
|
|
|
+ // 默认-8在视觉是协调的
|
|
|
+ marginTop: `${itemGap.value + 8}px`,
|
|
|
+ whiteSpace: 'nowrap'
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const getCellStyle = (cell: { inMonth: boolean; isCurrentDate: boolean }): CSSProperties => {
|
|
|
+ return {
|
|
|
+ ...styleMap.value?.itemsStyle,
|
|
|
+ display: 'flex',
|
|
|
+ alignItems: 'center',
|
|
|
+ justifyContent: 'center',
|
|
|
+ boxSizing: 'border-box',
|
|
|
+ position: 'relative',
|
|
|
+ backgroundColor: styleMap.value?.itemsStyle?.backgroundColor,
|
|
|
+ opacity: cell.inMonth ? 1 : 0.72
|
|
|
+ }
|
|
|
+}
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped lang="less">
|
|
|
+.calendar-body {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-weekdays,
|
|
|
+.calendar-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(7, minmax(0, 1fr));
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-weekdays {
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-weekday {
|
|
|
+ height: 30px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-grid {
|
|
|
+ grid-auto-rows: minmax(0, 1fr);
|
|
|
+ gap: 8px;
|
|
|
+ height: calc(100% - 40px);
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-cell {
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-current-ring {
|
|
|
+ position: absolute;
|
|
|
+ inset: 0;
|
|
|
+ border: 2px solid #2196f3;
|
|
|
+ box-sizing: border-box;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-nav-btn {
|
|
|
+ appearance: none;
|
|
|
+ cursor: default;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-select-row {
|
|
|
+ width: 100%;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.calendar-select-box {
|
|
|
+ flex: 1 1 0;
|
|
|
+}
|
|
|
+</style>
|