feat: add pull gestures for adjacent months
This commit is contained in:
parent
0c794ff718
commit
ce877719d3
|
|
@ -780,6 +780,11 @@ input:focus-visible {
|
|||
|
||||
.ledger-notice-slot { position:absolute; top:0; right:0; left:0; z-index:4; height:0; overflow:visible; pointer-events:none; }
|
||||
.conversion-notice { height:32px; display:flex; align-items:center; border-bottom:1px solid #eadfca; padding:4px 12px; background:#fff9ed; color:#775b24; font-size:11px; box-shadow:0 2px 8px rgba(58,43,17,.08); pointer-events:auto; }
|
||||
.ledger-pull-indicator { position:absolute; right:16px; left:16px; z-index:3; display:flex; align-items:center; justify-content:center; gap:5px; height:32px; border:1px solid #d4e3df; border-radius:8px; padding:0 10px; background:rgba(255,255,255,.96); color:#6d807b; box-shadow:0 4px 12px rgba(27,61,53,.1); font-size:11px; pointer-events:none; transform:translateY(calc((var(--pull-progress) - 1) * -20px)); }
|
||||
.ledger-pull-indicator.top { top:8px; }
|
||||
.ledger-pull-indicator.bottom { bottom:8px; }
|
||||
.ledger-pull-indicator.refresh,.ledger-pull-indicator.previous { color:#087f72; }
|
||||
.ledger-pull-indicator.next { color:#3478e5; }
|
||||
.conversion-notice span { display:flex; align-items:center; gap:6px; }
|
||||
|
||||
.amount-value {
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {
|
|||
ChevronLeft,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
ChevronUp,
|
||||
CircleUserRound,
|
||||
CloudOff,
|
||||
Copy,
|
||||
|
|
@ -44,6 +45,10 @@ const selectedMonth = ref(toMonthValue(new Date()));
|
|||
const monthSwipeStart = ref<{ x: number; y: number } | null>(null);
|
||||
const monthSwipeDelta = ref(0);
|
||||
const monthSwipeDragging = ref(false);
|
||||
const pullStartY = ref<number | null>(null);
|
||||
const pullDistance = ref(0);
|
||||
const pullEdge = ref<"top" | "bottom" | null>(null);
|
||||
const pullLoading = ref(false);
|
||||
let loadMoreObserver: IntersectionObserver | null = null;
|
||||
|
||||
const selectedMonthParts = computed(() => {
|
||||
|
|
@ -144,6 +149,26 @@ watch([ledgerContent, loadMoreTrigger], ([root, trigger]) => {
|
|||
|
||||
onBeforeUnmount(() => loadMoreObserver?.disconnect());
|
||||
|
||||
const pullAction = computed(() => {
|
||||
if (pullEdge.value === "bottom") return pullDistance.value >= 72 ? "next" : null;
|
||||
if (pullEdge.value !== "top") return null;
|
||||
if (pullDistance.value >= 112) return "previous";
|
||||
if (pullDistance.value >= 52) return "refresh";
|
||||
return null;
|
||||
});
|
||||
|
||||
const pullLabel = computed(() => {
|
||||
if (pullAction.value === "previous") return "松开以加载上一月流水";
|
||||
if (pullAction.value === "refresh") return "松开以刷新流水";
|
||||
if (pullEdge.value === "bottom") return "继续上拉加载下一月流水";
|
||||
return "继续下拉刷新流水";
|
||||
});
|
||||
|
||||
const pullProgress = computed(() => {
|
||||
const threshold = pullEdge.value === "bottom" ? 72 : pullDistance.value < 52 ? 52 : 112;
|
||||
return Math.min(1, pullDistance.value / threshold);
|
||||
});
|
||||
|
||||
function localDateKey(value: string) {
|
||||
const date = new Date(value);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
||||
|
|
@ -207,6 +232,59 @@ function cancelMonthSwipe() {
|
|||
monthSwipeDragging.value = false;
|
||||
}
|
||||
|
||||
function startLedgerPull(event: TouchEvent) {
|
||||
if (pullLoading.value) return;
|
||||
const content = ledgerContent.value;
|
||||
const touch = event.touches[0];
|
||||
if (!content || !touch) return;
|
||||
const atTop = content.scrollTop <= 0;
|
||||
const atBottom = content.scrollHeight > content.clientHeight + 4
|
||||
&& content.scrollTop + content.clientHeight >= content.scrollHeight - 2;
|
||||
pullEdge.value = atTop ? "top" : atBottom ? "bottom" : null;
|
||||
pullStartY.value = pullEdge.value ? touch.clientY : null;
|
||||
pullDistance.value = 0;
|
||||
}
|
||||
|
||||
function moveLedgerPull(event: TouchEvent) {
|
||||
const startY = pullStartY.value;
|
||||
const edge = pullEdge.value;
|
||||
const touch = event.touches[0];
|
||||
const content = ledgerContent.value;
|
||||
if (startY === null || !edge || !touch || !content) return;
|
||||
const delta = edge === "top" ? touch.clientY - startY : startY - touch.clientY;
|
||||
if (delta <= 0) {
|
||||
pullDistance.value = 0;
|
||||
return;
|
||||
}
|
||||
pullDistance.value = Math.min(144, delta * 0.55);
|
||||
}
|
||||
|
||||
async function finishLedgerPull() {
|
||||
const action = pullAction.value;
|
||||
pullStartY.value = null;
|
||||
if (!action || pullLoading.value) {
|
||||
pullEdge.value = null;
|
||||
pullDistance.value = 0;
|
||||
return;
|
||||
}
|
||||
pullLoading.value = true;
|
||||
try {
|
||||
if (action === "refresh" || action === "previous" || action === "next") await store.syncEntries();
|
||||
if (action === "previous") shiftMonth(-1);
|
||||
if (action === "next") shiftMonth(1);
|
||||
} finally {
|
||||
pullLoading.value = false;
|
||||
pullEdge.value = null;
|
||||
pullDistance.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function cancelLedgerPull() {
|
||||
pullStartY.value = null;
|
||||
pullEdge.value = null;
|
||||
pullDistance.value = 0;
|
||||
}
|
||||
|
||||
function shiftMonth(offset: number) {
|
||||
const selected = selectedMonthParts.value;
|
||||
if (!selected) return;
|
||||
|
|
@ -374,7 +452,26 @@ async function shareLedger() {
|
|||
</section>
|
||||
</header>
|
||||
|
||||
<section ref="ledgerContent" class="ledger-content" aria-label="账本流水">
|
||||
<section
|
||||
ref="ledgerContent"
|
||||
class="ledger-content"
|
||||
aria-label="账本流水"
|
||||
@touchstart.passive="startLedgerPull"
|
||||
@touchmove.passive="moveLedgerPull"
|
||||
@touchend="finishLedgerPull"
|
||||
@touchcancel="cancelLedgerPull"
|
||||
>
|
||||
<div
|
||||
v-if="pullEdge && pullDistance > 8"
|
||||
class="ledger-pull-indicator"
|
||||
:class="[pullEdge, pullAction, { loading: pullLoading }]"
|
||||
:style="{ '--pull-progress': pullProgress }"
|
||||
role="status"
|
||||
>
|
||||
<ChevronUp v-if="pullEdge === 'bottom'" :size="15" />
|
||||
<RefreshCw v-else :size="15" :class="{ spinning: pullLoading }" />
|
||||
<span>{{ pullLoading ? "正在加载" : pullLabel }}</span>
|
||||
</div>
|
||||
<div class="ledger-notice-slot" aria-live="polite">
|
||||
<div v-if="pendingEntryCount" class="sync-notice" role="status">
|
||||
<span><CloudOff :size="16" />{{ pendingEntryCount }} 条账目未同步云端</span>
|
||||
|
|
|
|||
Loading…
Reference in New Issue