feat: migrate CRM screens to Ant Design Vue and expand home dashboard
Preserve the local Ant UI work across views/components and add the new home dashboard chart panels before merging latest main.
This commit is contained in:
@@ -4,6 +4,15 @@ import type { RoutineCalendarDayDto } from '@mento/shared';
|
||||
import { ROUTINE_WEEKDAY_LABELS } from '@mento/shared';
|
||||
import { dateToJalali, toFaDigits } from '@/lib/jalali';
|
||||
import { DAY_STATE_LABELS, addDaysKey, keyToDate, todayKey, weekdayIndex } from '@/lib/routines';
|
||||
import {
|
||||
Button as AButton,
|
||||
Flex as AFlex,
|
||||
Typography,
|
||||
theme,
|
||||
} from 'ant-design-vue';
|
||||
|
||||
const { Text: ATypographyText } = Typography;
|
||||
const { token } = theme.useToken();
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -47,6 +56,39 @@ function heatLevel(d?: RoutineCalendarDayDto): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const STATE_BG: Record<string, string> = {
|
||||
complete: 'var(--ant-color-success-bg)',
|
||||
partial: 'var(--ant-color-warning-bg)',
|
||||
missed: 'var(--ant-color-error-bg)',
|
||||
empty: 'var(--ant-color-bg-container)',
|
||||
future: 'var(--ant-color-fill-quaternary)',
|
||||
};
|
||||
|
||||
const STATE_DOT: Record<string, string> = {
|
||||
complete: 'var(--ant-color-success)',
|
||||
partial: 'var(--ant-color-warning)',
|
||||
missed: 'var(--ant-color-error)',
|
||||
empty: 'var(--ant-color-text-quaternary)',
|
||||
future: 'var(--ant-color-text-quaternary)',
|
||||
};
|
||||
|
||||
const HEAT_BG: Record<number, string> = {
|
||||
0: 'var(--ant-color-fill-quaternary)',
|
||||
1: 'var(--ant-color-info-bg)',
|
||||
2: 'var(--ant-color-primary-bg)',
|
||||
3: 'var(--ant-color-success-bg)',
|
||||
4: 'var(--ant-color-success)',
|
||||
};
|
||||
|
||||
function cellKey(c: (typeof cells.value)[number]) {
|
||||
return c.future && !c.day?.total ? 'future' : (c.day?.state ?? 'empty');
|
||||
}
|
||||
|
||||
function cellBackground(c: (typeof cells.value)[number]) {
|
||||
if (props.mode === 'heat') return HEAT_BG[heatLevel(c.day)];
|
||||
return STATE_BG[cellKey(c)] ?? STATE_BG.empty;
|
||||
}
|
||||
|
||||
function title(c: (typeof cells.value)[number]) {
|
||||
const d = c.day;
|
||||
if (!d || !d.total) return 'بدون کار';
|
||||
@@ -55,211 +97,78 @@ function title(c: (typeof cells.value)[number]) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mgrid" :class="[mode, { compact }]">
|
||||
<div v-for="w in ROUTINE_WEEKDAY_LABELS" :key="w" class="wd">{{ compact ? w.charAt(0) : w }}</div>
|
||||
<template v-for="c in cells" :key="c.key">
|
||||
<span v-if="c.blank" class="cell blank" />
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="cell"
|
||||
:class="[
|
||||
mode === 'state' ? `s-${c.future && !c.day?.total ? 'future' : (c.day?.state ?? 'empty')}` : `h-${heatLevel(c.day)}`,
|
||||
{ sel: selected === c.key, today: c.key === today },
|
||||
]"
|
||||
:title="title(c)"
|
||||
@click="emit('select', c.key)"
|
||||
<a-flex vertical :gap="6">
|
||||
<a-flex :gap="6">
|
||||
<a-typography-text
|
||||
v-for="w in ROUTINE_WEEKDAY_LABELS"
|
||||
:key="w"
|
||||
type="secondary"
|
||||
strong
|
||||
:style="{ flex: '1 1 0', minWidth: 0, textAlign: 'center' }"
|
||||
>
|
||||
<span class="num">{{ toFaDigits(c.jd) }}</span>
|
||||
<template v-if="!compact && c.day?.total">
|
||||
<span class="frac">{{ toFaDigits(c.day.completed) }}/{{ toFaDigits(c.day.total) }}</span>
|
||||
<i v-if="mode === 'state'" class="dot" />
|
||||
</template>
|
||||
</button>
|
||||
</template>
|
||||
</div>
|
||||
{{ compact ? w.charAt(0) : w }}
|
||||
</a-typography-text>
|
||||
</a-flex>
|
||||
<a-flex wrap="wrap" :gap="6">
|
||||
<template v-for="c in cells" :key="c.key">
|
||||
<a-flex
|
||||
v-if="c.blank"
|
||||
:style="{ flex: '0 0 calc((100% - 36px) / 7)', minWidth: 0 }"
|
||||
/>
|
||||
<a-button
|
||||
v-else
|
||||
:title="title(c)"
|
||||
:style="{
|
||||
flex: '0 0 calc((100% - 36px) / 7)',
|
||||
minWidth: 0,
|
||||
height: compact ? '44px' : '74px',
|
||||
padding: '8px',
|
||||
background: cellBackground(c),
|
||||
borderColor:
|
||||
selected === c.key
|
||||
? token.colorPrimary
|
||||
: c.key === today
|
||||
? token.colorPrimaryBorder
|
||||
: undefined,
|
||||
boxShadow: selected === c.key ? `0 0 0 1px ${token.colorPrimary}` : undefined,
|
||||
}"
|
||||
@click="emit('select', c.key)"
|
||||
>
|
||||
<a-flex
|
||||
vertical
|
||||
align="start"
|
||||
justify="space-between"
|
||||
:style="{ width: '100%', height: '100%' }"
|
||||
>
|
||||
<a-typography-text
|
||||
strong
|
||||
:style="c.key === today ? { color: 'var(--ant-color-primary)' } : undefined"
|
||||
>
|
||||
{{ toFaDigits(c.jd) }}
|
||||
</a-typography-text>
|
||||
<a-flex
|
||||
v-if="!compact && c.day?.total"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
:style="{ width: '100%' }"
|
||||
>
|
||||
<a-typography-text type="secondary">
|
||||
{{ toFaDigits(c.day.completed) }}/{{ toFaDigits(c.day.total) }}
|
||||
</a-typography-text>
|
||||
<a-flex
|
||||
v-if="mode === 'state'"
|
||||
:style="{
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: token.borderRadiusLG,
|
||||
background: STATE_DOT[cellKey(c)],
|
||||
flexShrink: 0,
|
||||
}"
|
||||
/>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</a-button>
|
||||
</template>
|
||||
</a-flex>
|
||||
</a-flex>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.mgrid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wd {
|
||||
text-align: center;
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--muted);
|
||||
font-weight: var(--fw-semibold);
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
position: relative;
|
||||
min-height: 74px;
|
||||
padding: 8px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
background: var(--surface);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: right;
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.cell.blank {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.cell:not(.blank):hover {
|
||||
border-color: var(--line-strong);
|
||||
}
|
||||
|
||||
.cell.sel {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 2px var(--accent-soft);
|
||||
}
|
||||
|
||||
.cell.today .num {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.num {
|
||||
min-width: 24px;
|
||||
height: 24px;
|
||||
padding: 0 4px;
|
||||
border-radius: 99px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
font-size: var(--fs-sm);
|
||||
font-weight: var(--fw-semibold);
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.frac {
|
||||
font-size: var(--fs-xs);
|
||||
color: var(--muted);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
inset-inline-end: 10px;
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
background: var(--line-strong);
|
||||
}
|
||||
|
||||
/* وضعیت روز */
|
||||
.s-complete {
|
||||
background: color-mix(in srgb, var(--success-soft) 45%, var(--surface));
|
||||
}
|
||||
|
||||
.s-complete .dot {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
.s-partial {
|
||||
background: color-mix(in srgb, var(--warning-soft) 45%, var(--surface));
|
||||
}
|
||||
|
||||
.s-partial .dot {
|
||||
background: var(--warning);
|
||||
}
|
||||
|
||||
.s-missed {
|
||||
background: color-mix(in srgb, var(--danger-soft) 45%, var(--surface));
|
||||
}
|
||||
|
||||
.s-missed .dot {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.s-future {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
.s-empty {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* Heatmap */
|
||||
.h-0 {
|
||||
background: var(--surface-subtle);
|
||||
}
|
||||
|
||||
.h-1 {
|
||||
background: color-mix(in srgb, var(--accent) 14%, white);
|
||||
}
|
||||
|
||||
.h-2 {
|
||||
background: color-mix(in srgb, var(--accent) 32%, white);
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
background: color-mix(in srgb, var(--accent) 55%, white);
|
||||
}
|
||||
|
||||
.h-4 {
|
||||
background: color-mix(in srgb, var(--accent) 80%, white);
|
||||
}
|
||||
|
||||
.h-3 .num,
|
||||
.h-4 .num,
|
||||
.h-3 .frac,
|
||||
.h-4 .frac {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.compact {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.compact .cell {
|
||||
min-height: 0;
|
||||
aspect-ratio: 1;
|
||||
padding: 2px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-color: transparent;
|
||||
}
|
||||
|
||||
.compact .num {
|
||||
font-size: 0.7rem;
|
||||
min-width: 0;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.mgrid {
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.cell {
|
||||
min-height: 54px;
|
||||
padding: 4px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.frac {
|
||||
font-size: 0.62rem;
|
||||
}
|
||||
|
||||
.dot {
|
||||
position: static;
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user