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; }
|
.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; }
|
.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; }
|
.conversion-notice span { display:flex; align-items:center; gap:6px; }
|
||||||
|
|
||||||
.amount-value {
|
.amount-value {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import {
|
||||||
ChevronLeft,
|
ChevronLeft,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
ChevronRight,
|
ChevronRight,
|
||||||
|
ChevronUp,
|
||||||
CircleUserRound,
|
CircleUserRound,
|
||||||
CloudOff,
|
CloudOff,
|
||||||
Copy,
|
Copy,
|
||||||
|
|
@ -44,6 +45,10 @@ const selectedMonth = ref(toMonthValue(new Date()));
|
||||||
const monthSwipeStart = ref<{ x: number; y: number } | null>(null);
|
const monthSwipeStart = ref<{ x: number; y: number } | null>(null);
|
||||||
const monthSwipeDelta = ref(0);
|
const monthSwipeDelta = ref(0);
|
||||||
const monthSwipeDragging = ref(false);
|
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;
|
let loadMoreObserver: IntersectionObserver | null = null;
|
||||||
|
|
||||||
const selectedMonthParts = computed(() => {
|
const selectedMonthParts = computed(() => {
|
||||||
|
|
@ -144,6 +149,26 @@ watch([ledgerContent, loadMoreTrigger], ([root, trigger]) => {
|
||||||
|
|
||||||
onBeforeUnmount(() => loadMoreObserver?.disconnect());
|
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) {
|
function localDateKey(value: string) {
|
||||||
const date = new Date(value);
|
const date = new Date(value);
|
||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
|
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;
|
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) {
|
function shiftMonth(offset: number) {
|
||||||
const selected = selectedMonthParts.value;
|
const selected = selectedMonthParts.value;
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
|
|
@ -374,7 +452,26 @@ async function shareLedger() {
|
||||||
</section>
|
</section>
|
||||||
</header>
|
</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 class="ledger-notice-slot" aria-live="polite">
|
||||||
<div v-if="pendingEntryCount" class="sync-notice" role="status">
|
<div v-if="pendingEntryCount" class="sync-notice" role="status">
|
||||||
<span><CloudOff :size="16" />{{ pendingEntryCount }} 条账目未同步云端</span>
|
<span><CloudOff :size="16" />{{ pendingEntryCount }} 条账目未同步云端</span>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue