feat: animate month swipe feedback
This commit is contained in:
parent
44d6f37290
commit
b401dc808d
|
|
@ -327,7 +327,20 @@ input:focus-visible {
|
|||
box-shadow: 0 2px 6px rgba(32, 57, 51, 0.04);
|
||||
}
|
||||
|
||||
.list-toolbar > .list-toolbar-copy {
|
||||
.list-toolbar-content {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
transform: translateX(var(--month-swipe-offset, 0px));
|
||||
transition: transform 220ms cubic-bezier(.2,.8,.2,1);
|
||||
}
|
||||
|
||||
.list-toolbar-content.dragging { transition: none; }
|
||||
.month-swipe-hint { position:absolute; top:50%; left:50%; display:flex; align-items:center; gap:3px; transform:translate(-50%,-50%); border:1px solid #cfe0db; border-radius:999px; padding:4px 8px; background:rgba(255,255,255,.96); color:#087f72; box-shadow:0 4px 12px rgba(26,60,52,.12); font-size:11px; font-weight:700; pointer-events:none; }
|
||||
.month-swipe-hint.next { color:#3478e5; }
|
||||
|
||||
.list-toolbar-copy {
|
||||
min-width: 0;
|
||||
display: grid;
|
||||
gap: 3px;
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
import { formatCurrencyAmount, type LedgerEntry } from "@cents/domain";
|
||||
import {
|
||||
BookOpen,
|
||||
ChevronLeft,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
CircleUserRound,
|
||||
CloudOff,
|
||||
Copy,
|
||||
|
|
@ -40,6 +42,8 @@ const loadMoreTrigger = ref<HTMLElement | null>(null);
|
|||
const monthInput = ref<HTMLInputElement | null>(null);
|
||||
const selectedMonth = ref(toMonthValue(new Date()));
|
||||
const monthSwipeStart = ref<{ x: number; y: number } | null>(null);
|
||||
const monthSwipeDelta = ref(0);
|
||||
const monthSwipeDragging = ref(false);
|
||||
let loadMoreObserver: IntersectionObserver | null = null;
|
||||
|
||||
const selectedMonthParts = computed(() => {
|
||||
|
|
@ -158,7 +162,26 @@ function openMonthPicker() {
|
|||
|
||||
function startMonthSwipe(event: TouchEvent) {
|
||||
const touch = event.touches[0];
|
||||
if (touch) monthSwipeStart.value = { x: touch.clientX, y: touch.clientY };
|
||||
if (touch) {
|
||||
monthSwipeStart.value = { x: touch.clientX, y: touch.clientY };
|
||||
monthSwipeDelta.value = 0;
|
||||
monthSwipeDragging.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function moveMonthSwipe(event: TouchEvent) {
|
||||
const start = monthSwipeStart.value;
|
||||
const touch = event.touches[0];
|
||||
if (!start || !touch) return;
|
||||
const deltaX = touch.clientX - start.x;
|
||||
const deltaY = touch.clientY - start.y;
|
||||
if (Math.abs(deltaY) > Math.abs(deltaX) && Math.abs(deltaY) > 8) {
|
||||
monthSwipeDelta.value = 0;
|
||||
monthSwipeDragging.value = false;
|
||||
return;
|
||||
}
|
||||
monthSwipeDelta.value = Math.max(-96, Math.min(96, deltaX));
|
||||
monthSwipeDragging.value = Math.abs(deltaX) > 8;
|
||||
}
|
||||
|
||||
function finishMonthSwipe(event: TouchEvent) {
|
||||
|
|
@ -168,8 +191,20 @@ function finishMonthSwipe(event: TouchEvent) {
|
|||
if (!start || !touch) return;
|
||||
const deltaX = touch.clientX - start.x;
|
||||
const deltaY = touch.clientY - start.y;
|
||||
if (Math.abs(deltaX) < 48 || Math.abs(deltaX) <= Math.abs(deltaY)) return;
|
||||
if (Math.abs(deltaX) < 48 || Math.abs(deltaX) <= Math.abs(deltaY)) {
|
||||
monthSwipeDelta.value = 0;
|
||||
monthSwipeDragging.value = false;
|
||||
return;
|
||||
}
|
||||
shiftMonth(deltaX < 0 ? 1 : -1);
|
||||
monthSwipeDelta.value = 0;
|
||||
monthSwipeDragging.value = false;
|
||||
}
|
||||
|
||||
function cancelMonthSwipe() {
|
||||
monthSwipeStart.value = null;
|
||||
monthSwipeDelta.value = 0;
|
||||
monthSwipeDragging.value = false;
|
||||
}
|
||||
|
||||
function shiftMonth(offset: number) {
|
||||
|
|
@ -178,6 +213,13 @@ function shiftMonth(offset: number) {
|
|||
selectedMonth.value = toMonthValue(new Date(selected.year, selected.month + offset, 1));
|
||||
}
|
||||
|
||||
const monthSwipePreview = computed(() => {
|
||||
const selected = selectedMonthParts.value;
|
||||
if (!selected || !monthSwipeDelta.value) return "";
|
||||
const date = new Date(selected.year, selected.month + (monthSwipeDelta.value < 0 ? 1 : -1), 1);
|
||||
return `${date.getFullYear()}年${date.getMonth() + 1}月`;
|
||||
});
|
||||
|
||||
function dateHeading(value: string) {
|
||||
const date = new Date(`${value}T00:00:00`);
|
||||
const today = new Date();
|
||||
|
|
@ -344,24 +386,31 @@ async function shareLedger() {
|
|||
<span><Clock3 :size="15" />{{ pendingConversionCount }} 条外币账目等待换算</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list-toolbar" @touchstart.passive="startMonthSwipe" @touchend="finishMonthSwipe">
|
||||
<div class="list-toolbar-copy">
|
||||
<div class="list-toolbar-heading">
|
||||
<button class="list-month-switcher" type="button" @click="openMonthPicker">
|
||||
<strong>{{ monthTitle }}</strong>
|
||||
<ChevronDown :size="14" />
|
||||
</button>
|
||||
<input ref="monthInput" v-model="selectedMonth" class="native-month-input" type="month" aria-label="选择流水月份" tabindex="-1" />
|
||||
<span>{{ monthlyEntries.length }} 笔</span>
|
||||
</div>
|
||||
<div class="list-toolbar-totals">
|
||||
<span class="income">收入 {{ formatMoney(totals.income) }}</span>
|
||||
<span class="expense">支出 {{ formatMoney(totals.expense) }}</span>
|
||||
<div class="list-toolbar" @touchstart.passive="startMonthSwipe" @touchmove.passive="moveMonthSwipe" @touchend="finishMonthSwipe" @touchcancel="cancelMonthSwipe">
|
||||
<div class="list-toolbar-content" :class="{ dragging: monthSwipeDragging }" :style="{ '--month-swipe-offset': `${monthSwipeDelta * 0.22}px` }">
|
||||
<div class="list-toolbar-copy">
|
||||
<div class="list-toolbar-heading">
|
||||
<button class="list-month-switcher" type="button" @click="openMonthPicker">
|
||||
<strong>{{ monthTitle }}</strong>
|
||||
<ChevronDown :size="14" />
|
||||
</button>
|
||||
<input ref="monthInput" v-model="selectedMonth" class="native-month-input" type="month" aria-label="选择流水月份" tabindex="-1" />
|
||||
<span>{{ monthlyEntries.length }} 笔</span>
|
||||
</div>
|
||||
<div class="list-toolbar-totals">
|
||||
<span class="income">收入 {{ formatMoney(totals.income) }}</span>
|
||||
<span class="expense">支出 {{ formatMoney(totals.expense) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="icon-button list-action" type="button" aria-label="搜索流水" title="搜索流水">
|
||||
<Search :size="19" />
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="monthSwipeDragging" class="month-swipe-hint" :class="{ next: monthSwipeDelta < 0 }" aria-hidden="true">
|
||||
<ChevronRight v-if="monthSwipeDelta < 0" :size="14" />
|
||||
<ChevronLeft v-else :size="14" />
|
||||
<span>{{ monthSwipePreview }}</span>
|
||||
</div>
|
||||
<button class="icon-button list-action" type="button" aria-label="搜索流水" title="搜索流水">
|
||||
<Search :size="19" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section v-for="group in groupedEntries" :key="group.date" class="day-group">
|
||||
|
|
|
|||
Loading…
Reference in New Issue