Show jalali clock columns on desktop and pastel debtor/creditor asset boxes.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, ref, watch } from 'vue';
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
|
||||||
import {
|
import {
|
||||||
addJalaliMonths,
|
addJalaliMonths,
|
||||||
dateToJalali,
|
dateToJalali,
|
||||||
@@ -36,15 +36,23 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const WEEKDAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'] as const;
|
const WEEKDAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'] as const;
|
||||||
|
|
||||||
|
const MOBILE_MQ = '(max-width: 720px)';
|
||||||
|
|
||||||
const open = ref(false);
|
const open = ref(false);
|
||||||
const triggerRef = ref<HTMLButtonElement | null>(null);
|
const triggerRef = ref<HTMLButtonElement | null>(null);
|
||||||
const panelRef = ref<HTMLElement | null>(null);
|
const panelRef = ref<HTMLElement | null>(null);
|
||||||
|
const clockRef = ref<HTMLElement | null>(null);
|
||||||
|
const hourColRef = ref<HTMLElement | null>(null);
|
||||||
|
const minuteColRef = ref<HTMLElement | null>(null);
|
||||||
const top = ref(0);
|
const top = ref(0);
|
||||||
const left = ref(0);
|
const left = ref(0);
|
||||||
const todayJ = dateToJalali(new Date());
|
const todayJ = dateToJalali(new Date());
|
||||||
const cursor = ref<{ jy: number; jm: number }>({ jy: todayJ.jy, jm: todayJ.jm });
|
const cursor = ref<{ jy: number; jm: number }>({ jy: todayJ.jy, jm: todayJ.jm });
|
||||||
const draftHour = ref(9);
|
const draftHour = ref(9);
|
||||||
const draftMinute = ref(0);
|
const draftMinute = ref(0);
|
||||||
|
const clockFace = ref<'hour' | 'minute'>('hour');
|
||||||
|
const isMobile = ref(false);
|
||||||
|
const dragging = ref(false);
|
||||||
|
|
||||||
type Cell = {
|
type Cell = {
|
||||||
key: string;
|
key: string;
|
||||||
@@ -95,7 +103,8 @@ const monthTitle = computed(
|
|||||||
|
|
||||||
const hours = Array.from({ length: 24 }, (_, i) => i);
|
const hours = Array.from({ length: 24 }, (_, i) => i);
|
||||||
const minuteMarks = Array.from({ length: 12 }, (_, i) => i * 5);
|
const minuteMarks = Array.from({ length: 12 }, (_, i) => i * 5);
|
||||||
const minutes = computed(() => {
|
const columnMinutes = Array.from({ length: 60 }, (_, i) => i);
|
||||||
|
const clockMinutes = computed(() => {
|
||||||
if (!minuteMarks.includes(draftMinute.value)) {
|
if (!minuteMarks.includes(draftMinute.value)) {
|
||||||
return [...minuteMarks, draftMinute.value].sort((a, b) => a - b);
|
return [...minuteMarks, draftMinute.value].sort((a, b) => a - b);
|
||||||
}
|
}
|
||||||
@@ -107,6 +116,19 @@ const draftTimeLabel = computed(
|
|||||||
`${toFaDigits(String(draftHour.value).padStart(2, '0'))}:${toFaDigits(String(draftMinute.value).padStart(2, '0'))}`,
|
`${toFaDigits(String(draftHour.value).padStart(2, '0'))}:${toFaDigits(String(draftMinute.value).padStart(2, '0'))}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const hourHandInner = computed(() => draftHour.value >= 12);
|
||||||
|
const clockHandDeg = computed(() =>
|
||||||
|
clockFace.value === 'hour' ? (draftHour.value % 12) * 30 : draftMinute.value * 6,
|
||||||
|
);
|
||||||
|
|
||||||
|
function pad2(n: number) {
|
||||||
|
return String(n).padStart(2, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
function faPad(n: number) {
|
||||||
|
return toFaDigits(pad2(n));
|
||||||
|
}
|
||||||
|
|
||||||
function dialPoint(index: number, total: number, radius: number) {
|
function dialPoint(index: number, total: number, radius: number) {
|
||||||
const angle = (index / total) * 2 * Math.PI - Math.PI / 2;
|
const angle = (index / total) * 2 * Math.PI - Math.PI / 2;
|
||||||
return {
|
return {
|
||||||
@@ -116,22 +138,21 @@ function dialPoint(index: number, total: number, radius: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function hourDialStyle(hour: number) {
|
function hourDialStyle(hour: number) {
|
||||||
// حلقه داخلی ۰–۱۱، حلقه بیرونی ۱۲–۲۳
|
const ring = hour < 12 ? 92 : 58;
|
||||||
const ring = hour < 12 ? 46 : 72;
|
return dialPoint(hour % 12, 12, ring);
|
||||||
const index = hour % 12;
|
|
||||||
return dialPoint(index, 12, ring);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function minuteDialStyle(minute: number) {
|
function minuteDialStyle(minute: number) {
|
||||||
const index = minuteMarks.includes(minute)
|
const index = minuteMarks.includes(minute)
|
||||||
? minuteMarks.indexOf(minute)
|
? minuteMarks.indexOf(minute)
|
||||||
: Math.round(minute / 5) % 12;
|
: ((minute % 60) + 60) % 60 / 5;
|
||||||
return dialPoint(index, 12, 64);
|
return dialPoint(index, 12, 92);
|
||||||
}
|
}
|
||||||
|
|
||||||
function setHour(hour: number) {
|
function setHour(hour: number, opts?: { keepFace?: boolean }) {
|
||||||
draftHour.value = hour;
|
draftHour.value = hour;
|
||||||
applyTime();
|
applyTime();
|
||||||
|
if (isMobile.value && !opts?.keepFace) clockFace.value = 'minute';
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMinute(minute: number) {
|
function setMinute(minute: number) {
|
||||||
@@ -139,6 +160,57 @@ function setMinute(minute: number) {
|
|||||||
applyTime();
|
applyTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function valueFromPointer(ev: PointerEvent) {
|
||||||
|
const el = clockRef.value;
|
||||||
|
if (!el) return;
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
const dx = ev.clientX - (rect.left + rect.width / 2);
|
||||||
|
const dy = ev.clientY - (rect.top + rect.height / 2);
|
||||||
|
const dist = Math.hypot(dx, dy);
|
||||||
|
let deg = (Math.atan2(dy, dx) * 180) / Math.PI + 90;
|
||||||
|
if (deg < 0) deg += 360;
|
||||||
|
const index = Math.round(deg / 30) % 12;
|
||||||
|
if (clockFace.value === 'hour') {
|
||||||
|
const inner = dist < rect.width * 0.28;
|
||||||
|
setHour(inner ? index + 12 : index, { keepFace: dragging.value });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setMinute((index * 5) % 60);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClockPointerDown(ev: PointerEvent) {
|
||||||
|
dragging.value = true;
|
||||||
|
clockRef.value?.setPointerCapture(ev.pointerId);
|
||||||
|
valueFromPointer(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClockPointerMove(ev: PointerEvent) {
|
||||||
|
if (!dragging.value) return;
|
||||||
|
valueFromPointer(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClockPointerUp(ev: PointerEvent) {
|
||||||
|
if (!dragging.value) return;
|
||||||
|
dragging.value = false;
|
||||||
|
clockRef.value?.releasePointerCapture(ev.pointerId);
|
||||||
|
if (clockFace.value === 'hour') clockFace.value = 'minute';
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncViewport() {
|
||||||
|
isMobile.value = window.matchMedia(MOBILE_MQ).matches;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollListToSelected(list: HTMLElement | null) {
|
||||||
|
const item = list?.querySelector<HTMLElement>('.on');
|
||||||
|
if (!list || !item) return;
|
||||||
|
list.scrollTop = item.offsetTop - list.clientHeight / 2 + item.clientHeight / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
function scrollTimeColumns() {
|
||||||
|
scrollListToSelected(hourColRef.value);
|
||||||
|
scrollListToSelected(minuteColRef.value);
|
||||||
|
}
|
||||||
|
|
||||||
const cells = computed<Cell[]>(() => {
|
const cells = computed<Cell[]>(() => {
|
||||||
const { jy, jm } = cursor.value;
|
const { jy, jm } = cursor.value;
|
||||||
const first = jalaliToDate(jy, jm, 1);
|
const first = jalaliToDate(jy, jm, 1);
|
||||||
@@ -215,8 +287,8 @@ function place() {
|
|||||||
const panel = panelRef.value;
|
const panel = panelRef.value;
|
||||||
if (!trigger) return;
|
if (!trigger) return;
|
||||||
const rect = trigger.getBoundingClientRect();
|
const rect = trigger.getBoundingClientRect();
|
||||||
const width = panel?.offsetWidth || (props.mode === 'time' ? 340 : 320);
|
const width = panel?.offsetWidth || (props.mode === 'time' ? (isMobile.value ? 340 : 280) : 320);
|
||||||
const height = panel?.offsetHeight || (props.mode === 'time' ? 420 : 360);
|
const height = panel?.offsetHeight || (props.mode === 'time' ? (isMobile.value ? 460 : 320) : 360);
|
||||||
let nextLeft = rect.left;
|
let nextLeft = rect.left;
|
||||||
if (getComputedStyle(document.documentElement).direction === 'rtl') {
|
if (getComputedStyle(document.documentElement).direction === 'rtl') {
|
||||||
nextLeft = rect.right - width;
|
nextLeft = rect.right - width;
|
||||||
@@ -231,16 +303,20 @@ function place() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onWindowChange() {
|
function onWindowChange() {
|
||||||
|
syncViewport();
|
||||||
if (open.value) place();
|
if (open.value) place();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function show() {
|
async function show() {
|
||||||
if (props.disabled) return;
|
if (props.disabled) return;
|
||||||
syncDraftFrom(parsed.value);
|
syncDraftFrom(parsed.value);
|
||||||
|
clockFace.value = 'hour';
|
||||||
|
syncViewport();
|
||||||
open.value = true;
|
open.value = true;
|
||||||
await nextTick();
|
await nextTick();
|
||||||
place();
|
place();
|
||||||
await nextTick();
|
await nextTick();
|
||||||
|
scrollTimeColumns();
|
||||||
place();
|
place();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,12 +405,17 @@ watch(open, (value) => {
|
|||||||
window.addEventListener('keydown', onKeydown, true);
|
window.addEventListener('keydown', onKeydown, true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
dragging.value = false;
|
||||||
window.removeEventListener('resize', onWindowChange);
|
window.removeEventListener('resize', onWindowChange);
|
||||||
window.removeEventListener('scroll', onWindowChange, true);
|
window.removeEventListener('scroll', onWindowChange, true);
|
||||||
document.removeEventListener('pointerdown', onDocPointer, true);
|
document.removeEventListener('pointerdown', onDocPointer, true);
|
||||||
window.removeEventListener('keydown', onKeydown, true);
|
window.removeEventListener('keydown', onKeydown, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
watch(isMobile, () => {
|
||||||
|
if (open.value) void nextTick(place);
|
||||||
|
});
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => model.value,
|
() => model.value,
|
||||||
() => {
|
() => {
|
||||||
@@ -343,7 +424,13 @@ watch(
|
|||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
syncViewport();
|
||||||
|
window.matchMedia(MOBILE_MQ).addEventListener('change', syncViewport);
|
||||||
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
|
window.matchMedia(MOBILE_MQ).removeEventListener('change', syncViewport);
|
||||||
window.removeEventListener('resize', onWindowChange);
|
window.removeEventListener('resize', onWindowChange);
|
||||||
window.removeEventListener('scroll', onWindowChange, true);
|
window.removeEventListener('scroll', onWindowChange, true);
|
||||||
document.removeEventListener('pointerdown', onDocPointer, true);
|
document.removeEventListener('pointerdown', onDocPointer, true);
|
||||||
@@ -394,7 +481,11 @@ onBeforeUnmount(() => {
|
|||||||
id="jalali-picker"
|
id="jalali-picker"
|
||||||
ref="panelRef"
|
ref="panelRef"
|
||||||
class="panel"
|
class="panel"
|
||||||
:class="{ 'panel-time': mode === 'time' }"
|
:class="{
|
||||||
|
'panel-time': mode === 'time',
|
||||||
|
'is-mobile': isMobile,
|
||||||
|
'is-desktop': !isMobile,
|
||||||
|
}"
|
||||||
role="dialog"
|
role="dialog"
|
||||||
:style="{ top: `${top}px`, left: `${left}px` }"
|
:style="{ top: `${top}px`, left: `${left}px` }"
|
||||||
>
|
>
|
||||||
@@ -426,41 +517,111 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div v-if="mode === 'datetime' || mode === 'time'" class="time-dials">
|
<div v-if="mode === 'datetime' || mode === 'time'" class="time-box">
|
||||||
<p v-if="mode === 'time'" class="time-title">{{ draftTimeLabel }}</p>
|
<template v-if="isMobile">
|
||||||
<div class="dials">
|
<p class="md-label">{{ mode === 'time' ? 'انتخاب ساعت' : 'ساعت' }}</p>
|
||||||
<div class="dial-block">
|
<div class="md-digits" dir="ltr">
|
||||||
<span class="dial-label">ساعت</span>
|
<button
|
||||||
<div class="dial" role="listbox" aria-label="ساعت">
|
type="button"
|
||||||
<span class="dial-center">{{ toFaDigits(String(draftHour).padStart(2, '0')) }}</span>
|
class="md-chip"
|
||||||
|
:class="{ on: clockFace === 'hour' }"
|
||||||
|
aria-label="ساعت"
|
||||||
|
@click="clockFace = 'hour'"
|
||||||
|
>
|
||||||
|
{{ faPad(draftHour) }}
|
||||||
|
</button>
|
||||||
|
<span class="md-colon">:</span>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="md-chip"
|
||||||
|
:class="{ on: clockFace === 'minute' }"
|
||||||
|
aria-label="دقیقه"
|
||||||
|
@click="clockFace = 'minute'"
|
||||||
|
>
|
||||||
|
{{ faPad(draftMinute) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ref="clockRef"
|
||||||
|
class="md-clock"
|
||||||
|
role="slider"
|
||||||
|
:aria-label="clockFace === 'hour' ? 'ساعت' : 'دقیقه'"
|
||||||
|
@pointerdown.prevent="onClockPointerDown"
|
||||||
|
@pointermove.prevent="onClockPointerMove"
|
||||||
|
@pointerup.prevent="onClockPointerUp"
|
||||||
|
@pointercancel.prevent="onClockPointerUp"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="md-arm"
|
||||||
|
:class="{ short: clockFace === 'hour' && hourHandInner }"
|
||||||
|
:style="{ transform: `rotate(${clockHandDeg}deg)` }"
|
||||||
|
>
|
||||||
|
<i class="md-line" />
|
||||||
|
<i class="md-knob" />
|
||||||
|
</span>
|
||||||
|
<i class="md-hub" />
|
||||||
|
<template v-if="clockFace === 'hour'">
|
||||||
<button
|
<button
|
||||||
v-for="h in hours"
|
v-for="h in hours"
|
||||||
:key="`h-${h}`"
|
:key="`h-${h}`"
|
||||||
type="button"
|
type="button"
|
||||||
class="tick"
|
class="md-tick"
|
||||||
:class="{ on: draftHour === h, outer: h >= 12 }"
|
:class="{ on: draftHour === h, inner: h >= 12 }"
|
||||||
:style="hourDialStyle(h)"
|
:style="hourDialStyle(h)"
|
||||||
@click="setHour(h)"
|
tabindex="-1"
|
||||||
|
@click.stop="setHour(h)"
|
||||||
>
|
>
|
||||||
{{ toFaDigits(h) }}
|
{{ toFaDigits(h) }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</template>
|
||||||
</div>
|
<template v-else>
|
||||||
<div class="dial-block">
|
|
||||||
<span class="dial-label">دقیقه</span>
|
|
||||||
<div class="dial" role="listbox" aria-label="دقیقه">
|
|
||||||
<span class="dial-center">{{ toFaDigits(String(draftMinute).padStart(2, '0')) }}</span>
|
|
||||||
<button
|
<button
|
||||||
v-for="m in minutes"
|
v-for="m in clockMinutes"
|
||||||
:key="`m-${m}`"
|
:key="`m-${m}`"
|
||||||
type="button"
|
type="button"
|
||||||
class="tick"
|
class="md-tick"
|
||||||
:class="{ on: draftMinute === m }"
|
:class="{ on: draftMinute === m }"
|
||||||
:style="minuteDialStyle(m)"
|
:style="minuteDialStyle(m)"
|
||||||
@click="setMinute(m)"
|
tabindex="-1"
|
||||||
|
@click.stop="setMinute(m)"
|
||||||
>
|
>
|
||||||
{{ toFaDigits(String(m).padStart(2, '0')) }}
|
{{ faPad(m) }}
|
||||||
</button>
|
</button>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div v-else class="time-cols">
|
||||||
|
<p v-if="mode === 'time'" class="cols-title">{{ draftTimeLabel }}</p>
|
||||||
|
<div class="cols">
|
||||||
|
<div class="col">
|
||||||
|
<span class="col-label">ساعت</span>
|
||||||
|
<div ref="hourColRef" class="col-list" role="listbox" aria-label="ساعت">
|
||||||
|
<button
|
||||||
|
v-for="h in hours"
|
||||||
|
:key="`col-h-${h}`"
|
||||||
|
type="button"
|
||||||
|
class="col-item"
|
||||||
|
:class="{ on: draftHour === h }"
|
||||||
|
@click="setHour(h)"
|
||||||
|
>
|
||||||
|
{{ faPad(h) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<span class="col-label">دقیقه</span>
|
||||||
|
<div ref="minuteColRef" class="col-list" role="listbox" aria-label="دقیقه">
|
||||||
|
<button
|
||||||
|
v-for="m in columnMinutes"
|
||||||
|
:key="`col-m-${m}`"
|
||||||
|
type="button"
|
||||||
|
class="col-item"
|
||||||
|
:class="{ on: draftMinute === m }"
|
||||||
|
@click="setMinute(m)"
|
||||||
|
>
|
||||||
|
{{ faPad(m) }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -600,6 +761,8 @@ onBeforeUnmount(() => {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 120;
|
z-index: 120;
|
||||||
width: min(20.5rem, calc(100vw - 16px));
|
width: min(20.5rem, calc(100vw - 16px));
|
||||||
|
max-height: calc(100vh - 16px);
|
||||||
|
overflow: auto;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
||||||
@@ -611,6 +774,10 @@ onBeforeUnmount(() => {
|
|||||||
width: min(22.5rem, calc(100vw - 16px));
|
width: min(22.5rem, calc(100vw - 16px));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-time.is-desktop {
|
||||||
|
width: min(17.5rem, calc(100vw - 16px));
|
||||||
|
}
|
||||||
|
|
||||||
.nav {
|
.nav {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -681,80 +848,132 @@ onBeforeUnmount(() => {
|
|||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-dials {
|
.time-box {
|
||||||
margin-top: 0.55rem;
|
margin-top: 0.55rem;
|
||||||
display: grid;
|
|
||||||
gap: 0.65rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.time-title {
|
.grid + .time-box {
|
||||||
margin: 0;
|
margin-top: 0.7rem;
|
||||||
text-align: center;
|
padding-top: 0.65rem;
|
||||||
font-size: 1.35rem;
|
border-top: 1px solid #e2e8f0;
|
||||||
font-weight: 800;
|
|
||||||
color: #0f766e;
|
|
||||||
letter-spacing: 0.04em;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dials {
|
.md-label {
|
||||||
display: grid;
|
margin: 0 0 0.55rem;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 0.65rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dial-block {
|
|
||||||
display: grid;
|
|
||||||
gap: 0.4rem;
|
|
||||||
justify-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dial-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
|
font-size: 0.78rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dial {
|
.md-digits {
|
||||||
position: relative;
|
display: flex;
|
||||||
width: 148px;
|
align-items: center;
|
||||||
height: 148px;
|
justify-content: center;
|
||||||
border-radius: 50%;
|
gap: 0.35rem;
|
||||||
background:
|
margin-bottom: 0.85rem;
|
||||||
radial-gradient(circle at center, #ffffff 0 28%, transparent 29%),
|
|
||||||
radial-gradient(circle at center, #f0fdfa 0 70%, #ecfeff 100%);
|
|
||||||
border: 2px solid #99f6e4;
|
|
||||||
box-shadow: inset 0 0 0 1px rgba(13, 148, 136, 0.06);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.dial-center {
|
.md-chip {
|
||||||
|
min-width: 5.1rem;
|
||||||
|
height: 4.1rem;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #0f172a;
|
||||||
|
font: inherit;
|
||||||
|
font-size: 2.35rem;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-chip.on {
|
||||||
|
background: #ccfbf1;
|
||||||
|
color: #0f766e;
|
||||||
|
box-shadow: inset 0 0 0 2px #99f6e4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-colon {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f172a;
|
||||||
|
line-height: 1;
|
||||||
|
padding-bottom: 0.15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-clock {
|
||||||
|
position: relative;
|
||||||
|
width: 236px;
|
||||||
|
height: 236px;
|
||||||
|
margin: 0 auto;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #e2e8f0;
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-arm {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
width: 36px;
|
||||||
|
height: 110px;
|
||||||
|
margin: -110px 0 0 -18px;
|
||||||
|
transform-origin: center bottom;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-arm.short {
|
||||||
|
height: 76px;
|
||||||
|
margin-top: -76px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
height: calc(100% - 18px);
|
||||||
|
margin-left: -1px;
|
||||||
|
background: #0d9488;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-knob {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #0d9488;
|
||||||
|
}
|
||||||
|
|
||||||
|
.md-clock > .md-hub {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 50% auto auto 50%;
|
inset: 50% auto auto 50%;
|
||||||
transform: translate(-50%, -50%);
|
width: 8px;
|
||||||
width: 42px;
|
height: 8px;
|
||||||
height: 42px;
|
margin: -4px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
display: grid;
|
|
||||||
place-items: center;
|
|
||||||
background: #0d9488;
|
background: #0d9488;
|
||||||
color: #fff;
|
z-index: 3;
|
||||||
font-weight: 800;
|
pointer-events: none;
|
||||||
font-size: 0.95rem;
|
|
||||||
z-index: 2;
|
|
||||||
box-shadow: 0 6px 14px rgba(13, 148, 136, 0.28);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tick {
|
.md-tick {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
width: 28px;
|
width: 36px;
|
||||||
height: 28px;
|
height: 36px;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: #475569;
|
color: #334155;
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.72rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
@@ -763,20 +982,80 @@ onBeforeUnmount(() => {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tick.outer {
|
.md-tick.inner {
|
||||||
font-size: 0.68rem;
|
font-size: 0.74rem;
|
||||||
color: #64748b;
|
color: #64748b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tick:hover {
|
.md-tick.on {
|
||||||
background: #ccfbf1;
|
background: transparent;
|
||||||
color: #0f766e;
|
color: #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tick.on {
|
.time-cols {
|
||||||
background: #14b8a6;
|
display: grid;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cols-title {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #0f766e;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cols {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 0.55rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.3rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-label {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: #64748b;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-list {
|
||||||
|
height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f8fafc;
|
||||||
|
padding: 0.25rem;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-item {
|
||||||
|
width: 100%;
|
||||||
|
height: 36px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
color: #334155;
|
||||||
|
font: inherit;
|
||||||
|
font-weight: 700;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-item:hover {
|
||||||
|
background: #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.col-item.on {
|
||||||
|
background: #0d9488;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 4px 10px rgba(20, 184, 166, 0.35);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.foot {
|
.foot {
|
||||||
@@ -810,14 +1089,7 @@ onBeforeUnmount(() => {
|
|||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 420px) {
|
.panel.is-mobile .time-box {
|
||||||
.dials {
|
padding-top: 0.15rem;
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.dial {
|
|
||||||
width: 160px;
|
|
||||||
height: 160px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { api } from '@/lib/api';
|
|||||||
import { formatMoney } from '@/lib/money';
|
import { formatMoney } from '@/lib/money';
|
||||||
import {
|
import {
|
||||||
dateToJalali,
|
dateToJalali,
|
||||||
|
formatClockFa,
|
||||||
|
formatJalaliDay,
|
||||||
formatLocalDateInput,
|
formatLocalDateInput,
|
||||||
parseLocalInput,
|
parseLocalInput,
|
||||||
} from '@/lib/jalali';
|
} from '@/lib/jalali';
|
||||||
@@ -80,6 +82,14 @@ const hasMultipleConnections = computed(() => connections.value.length > 1);
|
|||||||
const goldBalance = computed(() => assets.value?.goldBalance ?? null);
|
const goldBalance = computed(() => assets.value?.goldBalance ?? null);
|
||||||
const rialBalance = computed(() => assets.value?.rialBalance ?? null);
|
const rialBalance = computed(() => assets.value?.rialBalance ?? null);
|
||||||
|
|
||||||
|
const fetchedAtLabel = computed(() => {
|
||||||
|
const raw = assets.value?.fetchedAt;
|
||||||
|
if (!raw) return '';
|
||||||
|
const d = new Date(raw);
|
||||||
|
if (Number.isNaN(d.getTime())) return '';
|
||||||
|
return `${formatJalaliDay(d)} ${formatClockFa(d)}`;
|
||||||
|
});
|
||||||
|
|
||||||
const mandehGroups = computed(() => {
|
const mandehGroups = computed(() => {
|
||||||
const items = assets.value?.mandehDetail ?? [];
|
const items = assets.value?.mandehDetail ?? [];
|
||||||
const groups: Record<string, typeof items> = {
|
const groups: Record<string, typeof items> = {
|
||||||
@@ -105,9 +115,9 @@ const mandehGroups = computed(() => {
|
|||||||
.map(([label, list]) => ({ label, items: list }));
|
.map(([label, list]) => ({ label, items: list }));
|
||||||
});
|
});
|
||||||
|
|
||||||
function balanceClass(v: number | null) {
|
function balanceTone(v: number | null) {
|
||||||
if (v == null || v === 0) return '';
|
if (v == null || v === 0) return 'tone-neutral';
|
||||||
return v > 0 ? 'credit' : 'debt';
|
return v > 0 ? 'tone-credit' : 'tone-debt';
|
||||||
}
|
}
|
||||||
|
|
||||||
function balanceLabel(v: number | null) {
|
function balanceLabel(v: number | null) {
|
||||||
@@ -479,29 +489,38 @@ onMounted(async () => {
|
|||||||
<template v-if="currentLink && assets">
|
<template v-if="currentLink && assets">
|
||||||
<section class="summary card">
|
<section class="summary card">
|
||||||
<h3>دارایی</h3>
|
<h3>دارایی</h3>
|
||||||
<div class="balance-row">
|
<div class="balance-grid">
|
||||||
<span>دارایی طلایی</span>
|
<article class="balance-box" :class="balanceTone(goldBalance)">
|
||||||
<div class="val" :class="balanceClass(goldBalance)" dir="ltr">
|
<span>دارایی طلایی</span>
|
||||||
|
<strong dir="ltr">
|
||||||
|
{{ formatAmt(goldBalance) }}
|
||||||
|
<em>(گرم)</em>
|
||||||
|
</strong>
|
||||||
<small v-if="balanceLabel(goldBalance)">{{ balanceLabel(goldBalance) }}</small>
|
<small v-if="balanceLabel(goldBalance)">{{ balanceLabel(goldBalance) }}</small>
|
||||||
{{ formatAmt(goldBalance) }}
|
</article>
|
||||||
<em>(گرم)</em>
|
<article class="balance-box" :class="balanceTone(rialBalance)">
|
||||||
</div>
|
<span>دارایی ریالی</span>
|
||||||
</div>
|
<strong dir="ltr">
|
||||||
<div class="balance-row">
|
{{ rialBalance != null ? formatMoney(Math.abs(rialBalance), 'IRR') : '—' }}
|
||||||
<span>دارایی ریالی</span>
|
</strong>
|
||||||
<div class="val" :class="balanceClass(rialBalance)" dir="ltr">
|
|
||||||
<small v-if="balanceLabel(rialBalance)">{{ balanceLabel(rialBalance) }}</small>
|
<small v-if="balanceLabel(rialBalance)">{{ balanceLabel(rialBalance) }}</small>
|
||||||
{{ rialBalance != null ? formatMoney(Math.abs(rialBalance), 'IRR') : '—' }}
|
</article>
|
||||||
</div>
|
<article
|
||||||
|
v-if="assets.totalGoldEquiv != null"
|
||||||
|
class="balance-box"
|
||||||
|
:class="balanceTone(assets.totalGoldEquiv)"
|
||||||
|
>
|
||||||
|
<span>دارایی کل به طلا</span>
|
||||||
|
<strong dir="ltr">
|
||||||
|
{{ formatAmt(assets.totalGoldEquiv) }}
|
||||||
|
<em>(گرم)</em>
|
||||||
|
</strong>
|
||||||
|
<small v-if="balanceLabel(assets.totalGoldEquiv)">{{
|
||||||
|
balanceLabel(assets.totalGoldEquiv)
|
||||||
|
}}</small>
|
||||||
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="assets.totalGoldEquiv != null" class="balance-row">
|
<p v-if="fetchedAtLabel" class="muted tiny">آخرین دریافت: {{ fetchedAtLabel }}</p>
|
||||||
<span>دارایی کل به طلا</span>
|
|
||||||
<div class="val" :class="balanceClass(assets.totalGoldEquiv)" dir="ltr">
|
|
||||||
{{ formatAmt(assets.totalGoldEquiv) }}
|
|
||||||
<em>(گرم)</em>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="muted tiny">آخرین دریافت: {{ assets.fetchedAt }}</p>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
@@ -746,32 +765,68 @@ onMounted(async () => {
|
|||||||
.summary h3 {
|
.summary h3 {
|
||||||
margin: 0 0 0.75rem;
|
margin: 0 0 0.75rem;
|
||||||
}
|
}
|
||||||
.balance-row {
|
.balance-grid {
|
||||||
display: flex;
|
display: grid;
|
||||||
justify-content: space-between;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
align-items: center;
|
gap: 0.65rem;
|
||||||
gap: 0.75rem;
|
|
||||||
padding: 0.45rem 0;
|
|
||||||
border-bottom: 1px dashed var(--line);
|
|
||||||
}
|
}
|
||||||
.balance-row:last-of-type {
|
.balance-box {
|
||||||
border-bottom: 0;
|
display: grid;
|
||||||
|
gap: 0.28rem;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.8rem 0.9rem;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
background: #f8fafc;
|
||||||
}
|
}
|
||||||
.val {
|
.balance-box span {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #64748b;
|
||||||
|
}
|
||||||
|
.balance-box strong {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 0.35rem;
|
gap: 0.3rem;
|
||||||
|
font-size: 1.15rem;
|
||||||
|
font-weight: 800;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
.balance-box em {
|
||||||
|
font-style: normal;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
.balance-box small {
|
||||||
|
font-size: 0.75rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
.val em {
|
.balance-box.tone-debt {
|
||||||
font-style: normal;
|
border-color: #f5b4b4;
|
||||||
font-weight: 500;
|
background: #fff1f2;
|
||||||
font-size: 0.75rem;
|
color: #9f1239;
|
||||||
color: var(--muted);
|
|
||||||
}
|
}
|
||||||
.val small {
|
.balance-box.tone-debt span,
|
||||||
font-size: 0.7rem;
|
.balance-box.tone-debt small {
|
||||||
font-weight: 600;
|
color: #be123c;
|
||||||
|
}
|
||||||
|
.balance-box.tone-credit {
|
||||||
|
border-color: #86efac;
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #166534;
|
||||||
|
}
|
||||||
|
.balance-box.tone-credit span,
|
||||||
|
.balance-box.tone-credit small {
|
||||||
|
color: #15803d;
|
||||||
|
}
|
||||||
|
.summary .tiny {
|
||||||
|
margin: 0.7rem 0 0;
|
||||||
|
}
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.balance-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.credit {
|
.credit {
|
||||||
color: #15803d;
|
color: #15803d;
|
||||||
|
|||||||
Reference in New Issue
Block a user